forked from BRETT/BRETT
283 lines
12 KiB
Vue
283 lines
12 KiB
Vue
<script setup lang="ts">
|
|
import type { Component } from 'vue'
|
|
import { toast } from 'vue-sonner'
|
|
import { CARD_BORDER_EFFECTS, type CardBorderEffectKey, isCardBorderEffect } from '~~/shared/utils/cardBorderEffects'
|
|
import { CARD_EFFECTS, type CardEffectKey } from '~~/shared/utils/cardEffects'
|
|
import CardBorderEffect from '@/components/effects/CardBorderEffect.vue'
|
|
import CardVignette from '@/components/effects/CardVignette.vue'
|
|
import FileDropzone from '@/components/FileDropzone.vue'
|
|
import EffectAcidSquares from '@/components/effects/EffectAcidSquares.vue'
|
|
import EffectAurora from '@/components/effects/EffectAurora.vue'
|
|
import EffectCrtWarp from '@/components/effects/EffectCrtWarp.vue'
|
|
import EffectDither from '@/components/effects/EffectDither.vue'
|
|
import EffectGradientWaves from '@/components/effects/EffectGradientWaves.vue'
|
|
import EffectLetterGlitch from '@/components/effects/EffectLetterGlitch.vue'
|
|
import EffectLineWaves from '@/components/effects/EffectLineWaves.vue'
|
|
import EffectParticles from '@/components/effects/EffectParticles.vue'
|
|
import EffectPlasma from '@/components/effects/EffectPlasma.vue'
|
|
import EffectScanner from '@/components/effects/EffectScanner.vue'
|
|
import EffectShapeGrid from '@/components/effects/EffectShapeGrid.vue'
|
|
import EffectSilk from '@/components/effects/EffectSilk.vue'
|
|
import EffectSoftAurora from '@/components/effects/EffectSoftAurora.vue'
|
|
import EffectWaves from '@/components/effects/EffectWaves.vue'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
|
import { Checkbox } from '@/components/ui/checkbox'
|
|
import { ColorPicker } from '@/components/ui/color-picker'
|
|
import { Input } from '@/components/ui/input'
|
|
import { Label } from '@/components/ui/label'
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from '@/components/ui/select'
|
|
|
|
const { user, refresh } = useAuth()
|
|
const { invalidate } = useUserSummaries()
|
|
const { t, locale, setLocale } = useI18n()
|
|
|
|
const color = ref(user.value?.color ?? '#6366f1')
|
|
const cardEffect = ref<string>(user.value?.cardEffect ?? 'none')
|
|
const cardEffectColor = ref<string>(user.value?.cardEffectColor ?? '#6366f1')
|
|
const cardEffectColorEnabled = ref(!!user.value?.cardEffectColor)
|
|
const cardVignette = ref(user.value?.cardVignette ?? false)
|
|
const cardBorderEffect = ref<string>(user.value?.cardBorderEffect ?? 'none')
|
|
const cardBorderEffectColor = ref<string>(user.value?.cardBorderEffectColor ?? '#6366f1')
|
|
const cardBorderEffectColorEnabled = ref(!!user.value?.cardBorderEffectColor)
|
|
const avatar = ref<File | null>(null)
|
|
const saving = ref(false)
|
|
|
|
const EFFECT_COMPONENTS: Record<CardEffectKey, Component> = {
|
|
'gradient-waves': EffectGradientWaves,
|
|
'aurora': EffectAurora,
|
|
'particles': EffectParticles,
|
|
'letter-glitch': EffectLetterGlitch,
|
|
'waves': EffectWaves,
|
|
'scanner': EffectScanner,
|
|
'acid-squares': EffectAcidSquares,
|
|
'crt-warp': EffectCrtWarp,
|
|
'dither': EffectDither,
|
|
'soft-aurora': EffectSoftAurora,
|
|
'silk': EffectSilk,
|
|
'shape-grid': EffectShapeGrid,
|
|
'plasma': EffectPlasma,
|
|
'line-waves': EffectLineWaves,
|
|
}
|
|
const previewEffect = computed(() =>
|
|
cardEffect.value !== 'none' && cardEffect.value in EFFECT_COMPONENTS
|
|
? EFFECT_COMPONENTS[cardEffect.value as CardEffectKey]
|
|
: null,
|
|
)
|
|
const previewBorder = computed<CardBorderEffectKey | null>(() =>
|
|
isCardBorderEffect(cardBorderEffect.value) ? cardBorderEffect.value : null,
|
|
)
|
|
// Effekt-Farben sind optional — ohne Häkchen fällt der Effekt auf die Namensfarbe
|
|
const previewEffectColor = computed(() =>
|
|
cardEffectColorEnabled.value ? cardEffectColor.value : color.value,
|
|
)
|
|
const previewBorderColor = computed(() =>
|
|
cardBorderEffectColorEnabled.value ? cardBorderEffectColor.value : color.value,
|
|
)
|
|
const showPreview = computed(() => previewEffect.value || previewBorder.value)
|
|
|
|
watch(() => user.value?.color, (c) => {
|
|
if (c) color.value = c
|
|
})
|
|
watch(() => user.value?.cardEffect, (e) => {
|
|
cardEffect.value = e ?? 'none'
|
|
})
|
|
watch(() => user.value?.cardEffectColor, (c) => {
|
|
cardEffectColorEnabled.value = !!c
|
|
if (c) cardEffectColor.value = c
|
|
})
|
|
watch(() => user.value?.cardVignette, (v) => {
|
|
cardVignette.value = v ?? false
|
|
})
|
|
watch(() => user.value?.cardBorderEffect, (e) => {
|
|
cardBorderEffect.value = e ?? 'none'
|
|
})
|
|
watch(() => user.value?.cardBorderEffectColor, (c) => {
|
|
cardBorderEffectColorEnabled.value = !!c
|
|
if (c) cardBorderEffectColor.value = c
|
|
})
|
|
|
|
async function save() {
|
|
if (saving.value || !user.value) return
|
|
saving.value = true
|
|
try {
|
|
const formData = new FormData()
|
|
formData.append('color', color.value)
|
|
formData.append('cardEffect', cardEffect.value === 'none' ? '' : cardEffect.value)
|
|
formData.append('cardEffectColor', cardEffectColorEnabled.value ? cardEffectColor.value : '')
|
|
formData.append('cardVignette', cardVignette.value ? '1' : '')
|
|
formData.append('cardBorderEffect', cardBorderEffect.value === 'none' ? '' : cardBorderEffect.value)
|
|
formData.append('cardBorderEffectColor', cardBorderEffectColorEnabled.value ? cardBorderEffectColor.value : '')
|
|
if (avatar.value) formData.append('avatar', avatar.value)
|
|
await $fetch('/api/users/me', { method: 'POST', body: formData })
|
|
await refresh()
|
|
// Gecachte Darstellungen (Hover-Card, Profil, Nutzerliste, Uploader-Karte)
|
|
// verwerfen, damit die neuen Einstellungen ohne Reload greifen
|
|
if (user.value) invalidate(user.value.id)
|
|
await refreshNuxtData()
|
|
avatar.value = null
|
|
toast.success(t('settings.saved'))
|
|
}
|
|
catch (err) {
|
|
const e = err as { data?: { statusMessage?: string } }
|
|
toast.error(e.data?.statusMessage ?? t('settings.saveFailed'))
|
|
}
|
|
finally {
|
|
saving.value = false
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="mx-auto w-full max-w-2xl px-4 lg:px-6">
|
|
<Card v-if="user">
|
|
<CardHeader>
|
|
<CardTitle>{{ $t('settings.title') }}</CardTitle>
|
|
<CardDescription>{{ $t('settings.desc') }}</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<form class="space-y-4" @submit.prevent="save">
|
|
<div class="space-y-1.5">
|
|
<Label>{{ $t('settings.language') }}</Label>
|
|
<Select :model-value="locale" @update:model-value="(v) => setLocale(v as string)">
|
|
<SelectTrigger class="w-full">
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="de">
|
|
Deutsch
|
|
</SelectItem>
|
|
<SelectItem value="en">
|
|
English
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>{{ $t('settings.nameColor') }}</Label>
|
|
<div class="flex items-center gap-3">
|
|
<ColorPicker :value="(color as `#${string}`)" type="hex" @value-change="(val: any) => color = val.hex">
|
|
<button
|
|
type="button"
|
|
class="h-9 w-14 cursor-pointer rounded-md border border-input shadow-sm"
|
|
:style="{ backgroundColor: color }"
|
|
:aria-label="$t('settings.pickColor')"
|
|
/>
|
|
</ColorPicker>
|
|
<span class="text-sm font-medium" :style="{ color }">{{ user.name }}</span>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>{{ $t('settings.cardEffect') }}</Label>
|
|
<Select v-model="cardEffect">
|
|
<SelectTrigger class="w-full">
|
|
<SelectValue :placeholder="$t('settings.noEffectPlaceholder')" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">
|
|
{{ $t('settings.noEffect') }}
|
|
</SelectItem>
|
|
<SelectItem v-for="e in CARD_EFFECTS" :key="e.key" :value="e.key">
|
|
{{ e.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<div v-if="cardEffect !== 'none'" class="flex flex-wrap items-center gap-x-5 gap-y-2 pt-1">
|
|
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
|
<Checkbox v-model="cardEffectColorEnabled" />
|
|
{{ $t('settings.ownEffectColor') }}
|
|
</label>
|
|
<ColorPicker v-if="cardEffectColorEnabled" :value="(cardEffectColor as `#${string}`)" type="hex" @value-change="(val: any) => cardEffectColor = val.hex">
|
|
<button
|
|
type="button"
|
|
class="h-7 w-12 cursor-pointer rounded-md border border-input shadow-sm"
|
|
:style="{ backgroundColor: cardEffectColor }"
|
|
:aria-label="$t('settings.pickEffectColor')"
|
|
/>
|
|
</ColorPicker>
|
|
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
|
<Checkbox v-model="cardVignette" />
|
|
{{ $t('settings.vignette') }}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>{{ $t('settings.cardBorder') }}</Label>
|
|
<Select v-model="cardBorderEffect">
|
|
<SelectTrigger class="w-full">
|
|
<SelectValue :placeholder="$t('settings.noEffectPlaceholder')" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">
|
|
{{ $t('settings.noEffect') }}
|
|
</SelectItem>
|
|
<SelectItem v-for="e in CARD_BORDER_EFFECTS" :key="e.key" :value="e.key">
|
|
{{ e.label }}
|
|
</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<div v-if="cardBorderEffect !== 'none'" class="flex items-center gap-3 pt-1">
|
|
<label class="flex cursor-pointer items-center gap-2 text-sm text-muted-foreground">
|
|
<Checkbox v-model="cardBorderEffectColorEnabled" />
|
|
{{ $t('settings.ownBorderColor') }}
|
|
</label>
|
|
<ColorPicker v-if="cardBorderEffectColorEnabled" :value="(cardBorderEffectColor as `#${string}`)" type="hex" @value-change="(val: any) => cardBorderEffectColor = val.hex">
|
|
<button
|
|
type="button"
|
|
class="h-7 w-12 cursor-pointer rounded-md border border-input shadow-sm"
|
|
:style="{ backgroundColor: cardBorderEffectColor }"
|
|
:aria-label="$t('settings.pickBorderColor')"
|
|
/>
|
|
</ColorPicker>
|
|
</div>
|
|
</div>
|
|
<div v-if="showPreview" class="space-y-1.5">
|
|
<Label>{{ $t('settings.preview') }}</Label>
|
|
<div class="relative h-24 overflow-hidden rounded-md border bg-popover">
|
|
<component :is="previewEffect" v-if="previewEffect" :color="previewEffectColor" />
|
|
<div v-if="previewEffect" class="absolute inset-0 bg-popover/60" />
|
|
<CardVignette v-if="previewEffect && cardVignette" />
|
|
<span class="absolute inset-0 flex items-center justify-center text-sm font-medium" :style="{ color }">
|
|
{{ user.name }}
|
|
</span>
|
|
<CardBorderEffect v-if="previewBorder" :effect="previewBorder" :color="previewBorderColor" />
|
|
</div>
|
|
<p class="text-xs text-muted-foreground">
|
|
{{ $t('settings.previewHint') }}
|
|
</p>
|
|
</div>
|
|
<div class="space-y-1.5">
|
|
<Label>{{ $t('settings.avatar') }}</Label>
|
|
<div v-if="user.avatarUrl && !avatar" class="mb-2 flex items-center gap-2">
|
|
<NuxtImg
|
|
:src="user.avatarUrl"
|
|
:alt="$t('settings.currentAvatar')"
|
|
width="48"
|
|
height="48"
|
|
fit="cover"
|
|
class="size-12 rounded-lg"
|
|
/>
|
|
<span class="text-xs text-muted-foreground">{{ $t('settings.currentAvatar') }}</span>
|
|
</div>
|
|
<FileDropzone v-model="avatar" accept="image/*" :hint="t('upload.avatarHint')" />
|
|
</div>
|
|
<Button type="submit" class="w-full" :disabled="saving">
|
|
{{ saving ? $t('common.saving') : $t('common.save') }}
|
|
</Button>
|
|
</form>
|
|
</CardContent>
|
|
</Card>
|
|
<p v-else class="text-center text-muted-foreground">
|
|
<NuxtLink to="/login" class="text-primary underline">
|
|
{{ $t('common.loginLink') }}
|
|
</NuxtLink>{{ $t('settings.loginToChange') }}
|
|
</p>
|
|
</div>
|
|
</template>
|