traxxx-web/pages/users/@username/+Page.vue

537 lines
10 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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(--shadow-strong-30);
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(--glass-weak-40);
&:hover {
border-color: var(--glass-weak-30);
}
}
.alert-triggers {
display: flex;
align-items: center;
gap: .5rem;
margin-right: .75rem;
.icon {
fill: var(--glass-weak-40);
}
.icon.trigger {
fill: var(--glass-weak-10);
}
}
.alert-details {
padding: .25rem 0;
flex-grow: 1;
line-height: 2.5;
color: var(--glass);
&.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(--glass);
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(--glass-weak-10);
.icon {
height: 100%;
padding: 0 .75rem;
fill: var(--glass);
&: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>