<template> <div class="page"> <div class="profile"> <div class="profile-header"> <div class="user"> <img v-if="profile.avatar" :src="profile.avatar" class="avatar" > <h2 class="username ellipsis">{{ profile.username }}</h2> </div> <span class="age">{{ formatDistanceStrict(Date.now(), profile.createdAt) }}</span> </div> <section class="profile-section"> <div class="section-header"> <h3 class="heading">Stashes</h3> <button v-if="profile.id === user?.id" class="button" @click="showStashDialog = true" > <Icon icon="plus3" /> <span class="button-label">New stash</span> </button> </div> <Dialog v-if="showStashDialog" title="New stash" @close="showStashDialog = false" @open="stashNameInput?.focus()" > <form class="dialog-body" @submit.prevent="createStash" > <input ref="stashNameInput" v-model="stashName" maxlength="24" placeholder="Stash name" class="input" > <button class="button button-submit" >Create stash</button> </form> </Dialog> <ul class="stashes nolist"> <li v-for="stash in profile.stashes" :key="`stash-${stash.id}`" > <StashTile :stash="stash" :profile="profile" @reload="reloadProfile" /> </li> </ul> </section> <section v-if="profile.id === user?.id" class="profile-section" > <div class="section-header"> <h3 class="heading">Alerts</h3> <button class="button" @click="showAlertDialog = true" > <Icon icon="plus3" /> <span class="button-label">New alert</span> </button> </div> <ul class="alerts nolist"> <li v-for="alert in alerts" :key="`alert-${alert.id}`" class="alert" > <div class="alert-triggers"> <Icon v-tooltip="alert.notify ? 'Notify in traxxx' : undefined" icon="bell2" :class="{ trigger: alert.notify }" /> <Icon v-if="alert.stashes.some((stash) => !stash.isPrimary)" v-tooltip="`Add to ${alert.stashes.map((stash) => stash.name).join(', ')}`" icon="folder-heart" class="trigger" /> <Icon v-else v-tooltip="alert.stashes.length > 0 ? 'Add to Favorites' : undefined" icon="heart7" :class="{ trigger: alert.stashes.length > 0 }" /> <!-- <Icon icon="envelop5" title="E-mail me" :class="{ trigger: alert.email }" /> --> </div> <div class="alert-details" :class="{ and: alert.and.fields, or: !alert.and.fields }" > <span v-if="alert.tags.length > 0" class="alert-detail alert-tags" :class="{ and: alert.and.tags, or: !alert.and.tags }" > <span class="alert-values"> <span v-for="tag in alert.tags" :key="`tag-${alert.id}-${tag.id}`" class="alert-key" > <a :href="`/tag/${tag.slug}`" class="alert-value link" >{{ tag.name }}</a> </span> </span> </span> <span v-if="alert.actors.length > 0" class="alert-detail alert-actors" :class="{ and: alert.and.actors, or: !alert.and.actors }" > <span class="alert-values">with <span v-for="actor in alert.actors" :key="`actor-${alert.id}-${actor.id}`" class="alert-key" > <a :href="`/actor/${actor.id}/${actor.slug}`" class="alert-value link" >{{ actor.name }}</a> </span> </span> </span> <span v-if="alert.entities.length > 0" class="alert-detail alert-entities or" > <span class="alert-values">for <span v-for="entity in alert.entities" :key="`entity-${alert.id}-${entity.id}`" class="alert-key" > <a :href="`/${entity.type}/${entity.slug}`" class="alert-value link" > <Icon v-if="entity.type === 'network'" icon="device_hub" />{{ entity.name }} </a> </span> </span> </span> <span v-if="alert.matches.length > 0" class="alert-detail alert-matches" :class="{ and: alert.and.matches, or: !alert.and.matches }" > <span class="alert-values">matching <span v-for="match in alert.matches" :key="`match-${alert.id}-${match.id}`" class="alert-key" > <span class="alert-value">{{ match.property }}: <span class="alert-regex" title="If your original expression was not a /regular expression/, it was converted, and new characters may have been added for syntactical purposes. These characters do not alter the function of the expression; they ensure it." >{{ match.expression }}</span> </span> </span> </span> </span> </div> <div class="alert-actions"> <span class="alert-id" title="Alert ID" >#{{ alert.id }}</span> <Icon icon="bin" @click="removeAlert(alert)" /> </div> </li> </ul> <AlertDialog v-if="showAlertDialog" @close="showAlertDialog = false; reloadAlerts();" /> </section> </div> </div> </template> <script setup> import { ref, inject } from 'vue'; import { formatDistanceStrict } from 'date-fns'; import { get, post, del } from '#/src/api.js'; import StashTile from '#/components/stashes/tile.vue'; import Dialog from '#/components/dialog/dialog.vue'; import AlertDialog from '#/components/alerts/create.vue'; const pageContext = inject('pageContext'); const user = pageContext.user; const profile = ref(pageContext.pageProps.profile); const alerts = ref(pageContext.pageProps.alerts); const stashName = ref(null); const stashNameInput = ref(null); const done = ref(true); const showStashDialog = ref(false); const showAlertDialog = ref(false); async function reloadProfile() { profile.value = await get(`/users/${profile.value.id}`); } async function createStash() { if (done.value === false) { return; } done.value = false; try { await post('/stashes', { name: stashName.value, public: false, }, { successFeedback: `Created stash '${stashName.value}'`, errorFeedback: `Failed to create stash '${stashName.value}'`, appendErrorMessage: true, }); } finally { done.value = true; } showStashDialog.value = false; stashName.value = null; await reloadProfile(); } async function reloadAlerts() { alerts.value = await get('/alerts'); } async function removeAlert(alert) { if (done.value === false) { return; } done.value = false; const alertLabel = [ ...alert.actors.map((actor) => actor.name), ...alert.tags.map((tag) => tag.name), ...alert.entities.map((entity) => entity.name), ...alert.matches.map((match) => match.expression), ].filter(Boolean).join(', '); try { await del(`/alerts/${alert.id}`, { undoFeedback: `Removed alert for '${alertLabel}'`, errorFeedback: `Failed to remove alert for '${alertLabel}'`, appendErrorMessage: true, }); await reloadAlerts(); } finally { done.value = true; } } </script> <style scoped> .page { display: flex; flex-grow: 1; justify-content: center; background: var(--background-base-10); } .profile { width: 1200px; max-width: 100%; } .profile-header { display: flex; justify-content: space-between; align-items: center; padding: .5rem 1rem; color: var(--highlight-strong-30); background: var(--grey-dark-40); border-radius: 0 0 .5rem .5rem; } .user { display: flex; overflow: hidden; } .username { margin: 0; font-size: 1.25rem; } .age { display: flex; flex-shrink: 0; font-size: .9rem; .icon { width: .9rem; fill: var(--highlight-strong-20); margin-right: .75rem; transform: translateY(-1px); } } .avatar { width: 1.5rem; height: 1.5rem; border-radius: .25rem; margin-right: 1rem; } .section-header { display: flex; align-items: center; justify-content: space-between; padding: .5rem .5rem .5rem .5rem; .button { margin-left: 1rem; } } .heading { margin: 0; font-size: 1.1rem; color: var(--primary); } .stashes { display: grid; grid-template-columns: repeat(auto-fill, minmax(12rem, 1fr)); gap: 1rem; padding: 0 .5rem 1rem .5rem; } .alerts { width: 100%; } .alert { padding: 0 0 0 .5rem; display: flex; align-items: stretch; border-bottom: solid 1px var(--shadow-weak-40); &:hover { border-color: var(--shadow-weak-30); } } .alert-triggers { display: flex; align-items: center; gap: .5rem; margin-right: .75rem; .icon { fill: var(--shadow-weak-40); } .icon.trigger { fill: var(--shadow-weak-10); } } .alert-details { padding: .25rem 0; flex-grow: 1; line-height: 2.5; color: var(--shadow); &.and .alert-detail:not(:last-child):after { content: ' and '; } &.or .alert-detail:not(:last-child):after { content: ' or '; } } .alert-value { color: var(--text); .icon { margin-right: .25rem; fill: var(--shadow); transform: translateY(2px); } } .alert-values { padding: .5rem .5rem; border-bottom: solid 1px var(--primary-light-20); border-radius: .3rem; } .alert-detail { &.and .alert-key:not(:last-child):after { content: ' and '; } &.or .alert-key:not(:last-child):after { content: ' or '; } } .alert-regex { &:before, &:after { content: '╱'; padding: 0 .1rem; color: var(--primary-light-20); } } .alert-actions { display: flex; align-items: center; font-size: .9rem; color: var(--shadow-weak-10); .icon { height: 100%; padding: 0 .75rem; fill: var(--shadow); &:hover { cursor: pointer; fill: var(--primary); } } } .dialog-body { padding: 1rem; .input { margin-bottom: .5rem; } } @media(--compact) { .profile-header { border-radius: 0; } .section-header { padding: .5rem 1rem .5rem 1rem; } .stashes { padding: 0 1rem 1rem 1rem; } .alert { padding: 0 .5rem 0 1rem; } } @media(--small-30) { .section-header { padding: .5rem .5rem .5rem .5rem; } .stashes { padding: 0 .5rem 1rem .5rem; } .age .icon { display: none; } } @media(--small-50) { .age { display: none; } } </style>