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

537 lines
10 KiB
Vue
Raw Normal View History

2024-02-29 04:08:54 +00:00
<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"
>
2024-05-28 03:49:28 +00:00
<div class="alert-triggers">
<Icon
v-tooltip="alert.notify ? 'Notify in traxxx' : undefined"
2024-05-28 03:49:28 +00:00
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(', ')}`"
2024-05-28 03:49:28 +00:00
icon="folder-heart"
class="trigger"
/>
<Icon
v-else
v-tooltip="alert.stashes.length > 0 ? 'Add to Favorites' : undefined"
2024-05-28 03:49:28 +00:00
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"
2024-05-28 03:49:28 +00:00
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"
2024-05-28 03:49:28 +00:00
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"
2024-05-28 03:49:28 +00:00
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"
2024-05-28 03:49:28 +00:00
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">
2024-05-28 03:49:28 +00:00
<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>
2024-02-29 04:08:54 +00:00
</div>
</template>
<script setup>
import { ref, inject } from 'vue';
import { formatDistanceStrict } from 'date-fns';
import { get, post, del } from '#/src/api.js';
2024-02-29 04:08:54 +00:00
import StashTile from '#/components/stashes/tile.vue';
import Dialog from '#/components/dialog/dialog.vue';
import AlertDialog from '#/components/alerts/create.vue';
2024-02-29 04:08:54 +00:00
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;
}
}
2024-02-29 04:08:54 +00:00
</script>
<style scoped>
.page {
display: flex;
flex-grow: 1;
justify-content: center;
background: var(--background-base-10);
}
.profile {
width: 1200px;
max-width: 100%;
}
2024-02-29 04:08:54 +00:00
.profile-header {
display: flex;
justify-content: space-between;
2024-02-29 04:08:54 +00:00
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;
2024-02-29 04:08:54 +00:00
}
.username {
margin: 0;
font-size: 1.25rem;
2024-02-29 04:08:54 +00:00
}
.age {
display: flex;
flex-shrink: 0;
font-size: .9rem;
.icon {
width: .9rem;
fill: var(--highlight-strong-20);
margin-right: .75rem;
transform: translateY(-1px);
}
}
2024-02-29 04:08:54 +00:00
.avatar {
width: 1.5rem;
height: 1.5rem;
2024-02-29 04:08:54 +00:00
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));
2024-03-17 04:27:43 +00:00
gap: 1rem;
padding: 0 .5rem 1rem .5rem;
}
.alerts {
width: 100%;
}
.alert {
2024-05-28 03:49:28 +00:00
padding: 0 0 0 .5rem;
display: flex;
align-items: stretch;
2024-05-28 03:49:28 +00:00
border-bottom: solid 1px var(--shadow-weak-40);
2024-05-28 03:49:28 +00:00
&:hover {
border-color: var(--shadow-weak-30);
}
2024-05-28 03:49:28 +00:00
}
2024-05-28 03:49:28 +00:00
.alert-triggers {
display: flex;
align-items: center;
gap: .5rem;
margin-right: .75rem;
2024-05-28 03:49:28 +00:00
.icon {
fill: var(--shadow-weak-40);
}
2024-05-27 01:12:38 +00:00
2024-05-28 03:49:28 +00:00
.icon.trigger {
fill: var(--shadow-weak-10);
2024-05-27 01:12:38 +00:00
}
}
.alert-details {
padding: .25rem 0;
flex-grow: 1;
line-height: 2.5;
2024-05-28 03:49:28 +00:00
color: var(--shadow);
2024-05-28 03:49:28 +00:00
&.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 {
2024-05-28 03:49:28 +00:00
display: flex;
align-items: center;
font-size: .9rem;
color: var(--shadow-weak-10);
.icon {
height: 100%;
2024-05-28 03:49:28 +00:00
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;
}
2024-05-28 03:49:28 +00:00
.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;
}
}
2024-02-29 04:08:54 +00:00
</style>