Refactor app architecture and clean local artifacts

This commit is contained in:
AzuTear
2026-06-24 23:43:14 +02:00
parent 17134b3b82
commit fef1d36fe8
274 changed files with 37724 additions and 6065 deletions
+123 -184
View File
@@ -1,40 +1,46 @@
<script setup lang="ts">
import { computed, reactive, ref } from 'vue'
import { RouterLink, useRoute } from 'vue-router'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import AppShellAccountModals from './AppShellAccountModals.vue'
import { useAuthStore } from '../stores/auth'
import { usePhases } from '../composables/usePhases'
import { useAwardsStore } from '../stores/awards'
import type { AuthRole } from '../types/awards'
const route = useRoute()
const router = useRouter()
const authStore = useAuthStore()
const { infoForPhaseKey } = usePhases()
const awardsStore = useAwardsStore()
const loginForm = reactive({
twitchUserId: 'jayuhime_demo',
twitchUserId: 'jayuhime_viewer',
displayName: 'Jayuhime',
role: 'viewer' as 'viewer' | 'admin',
role: 'viewer' as AuthRole,
})
const accountOpen = ref(false)
const deleteConfirm = ref(false)
const privacyOpen = ref(false)
const accountActionError = ref('')
defineExpose({ privacyOpen })
const navItems = [
{ label: 'Home', to: '/' },
{ label: 'Nominierung', to: '/nominations', requiresAuth: true, phase: 'nomination' },
{ label: 'Voting', to: '/voting', requiresAuth: true, phase: 'voting' },
{ label: 'Clips', to: '/clips', requiresAuth: true, phase: 'nomination' },
{ label: 'Gewinner', to: '/winners' },
{ label: 'Landingpage', to: '/' },
]
const visibleNavItems = computed(() =>
navItems.filter((item) => {
if (item.requiresAuth && !authStore.isLoggedIn) return false
if (item.phase && infoForPhaseKey(item.phase).state !== 'active') return false
return true
}),
const visibleNavItems = computed(() => navItems)
const privacyContent = computed(
() => awardsStore.overview.siteContent.privacyPolicyContent || awardsStore.adminSiteSettings.privacyPolicyContent,
)
const privacyEmail = computed(
() => awardsStore.overview.siteContent.privacyEmail || awardsStore.adminSiteSettings.privacyEmail,
)
const privacyContentBlocks = computed(() =>
privacyContent.value
.split(/\n{2,}/)
.map((block) => block.trim())
.filter(Boolean),
)
async function doLogin() {
@@ -44,9 +50,47 @@ async function doLogin() {
}
async function doLogout() {
closeAccountModal()
accountActionError.value = ''
await authStore.logout()
await router.replace({ name: 'login' })
}
async function deleteMyData() {
accountActionError.value = ''
try {
await authStore.deleteMyData()
accountOpen.value = false
deleteConfirm.value = false
await awardsStore.loadHomeData()
await router.replace({ name: 'login' })
} catch (error) {
accountActionError.value = error instanceof Error
? error.message
: 'Deine Daten konnten gerade nicht gelöscht werden.'
}
}
function closeAccountModal() {
accountOpen.value = false
deleteConfirm.value = false
await authStore.logout()
}
function openPrivacyModal() {
privacyOpen.value = true
}
function closePrivacyModal() {
privacyOpen.value = false
}
function requestAccountDeletion() {
deleteConfirm.value = true
}
function cancelAccountDeletion() {
deleteConfirm.value = false
accountActionError.value = ''
}
function isActive(to: string) {
@@ -61,43 +105,49 @@ const linkInactive = linkBase + 'background:transparent;color:#6f6685;font-weigh
<template>
<div style="min-height:100vh;overflow-x:hidden;font-family:'Outfit',sans-serif;line-height:1.5;background:#f4eefb;color:#3f3556;">
<template v-if="route.name === 'home' || route.meta.bareShell">
<slot />
</template>
<template v-else>
<!-- Nav -->
<nav style="position:sticky;top:0;z-index:50;background:rgba(248,243,254,.86);border-bottom:1px solid #ece2fa;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);">
<div style="max-width:1200px;margin:0 auto;padding:0 24px;height:64px;display:flex;align-items:center;gap:16px;">
<nav class="app-shell-topbar" style="position:sticky;top:0;z-index:50;background:rgba(248,243,254,.86);border-bottom:1px solid #ece2fa;backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);">
<div class="app-shell-topbar__inner" style="width:100%;padding:0 24px;height:64px;display:flex;align-items:center;gap:16px;">
<!-- Logo -->
<RouterLink to="/" style="display:flex;align-items:center;gap:10px;text-decoration:none;flex:none;">
<RouterLink to="/" class="app-shell-brand" style="display:flex;align-items:center;gap:10px;text-decoration:none;flex:none;">
<div style="width:36px;height:36px;border-radius:10px;background:linear-gradient(135deg,#8b6cdb,#e7b13e);display:flex;align-items:center;justify-content:center;color:#fff;font-size:18px;box-shadow:0 6px 14px rgba(139,108,219,.3);flex:none;"></div>
<span style="font-family:'Fredoka',sans-serif;font-size:17px;font-weight:600;color:#3f3556;">VTuber Star Award</span>
<span class="app-shell-brand__text" style="font-family:'Fredoka',sans-serif;font-size:17px;font-weight:600;color:#3f3556;">VTuber Star Award</span>
</RouterLink>
<!-- Links -->
<nav style="display:flex;align-items:center;gap:4px;margin-left:auto;">
<nav class="app-shell-links" style="display:flex;align-items:center;gap:4px;margin-left:auto;justify-content:flex-end;">
<RouterLink
v-for="item in visibleNavItems"
:key="item.to"
:to="item.to"
class="app-shell-link app-shell-link--secondary"
:style="isActive(item.to) ? linkActive : linkInactive"
>{{ item.label }}</RouterLink>
<RouterLink
v-if="authStore.isAdmin"
to="/admin"
class="app-shell-link"
:style="route.path.startsWith('/admin') ? linkActive : linkInactive"
>Admin</RouterLink>
</nav>
<!-- Auth -->
<div style="display:flex;align-items:center;gap:8px;">
<div class="app-shell-auth" style="display:flex;align-items:center;gap:8px;">
<template v-if="authStore.session">
<button
class="app-shell-account-button"
@click="accountOpen = true"
style="display:inline-flex;align-items:center;gap:8px;padding:9px 14px;border-radius:999px;border:none;background:rgba(139,108,219,.08);color:#6a4fb8;font-family:'Outfit',sans-serif;font-weight:600;font-size:14px;cursor:pointer;"
>
<span style="width:28px;height:28px;border-radius:50%;background:linear-gradient(135deg,#8b6cdb,#7355c8);color:#fff;display:flex;align-items:center;justify-content:center;font-size:12px;font-weight:700;flex:none;">
{{ authStore.session.displayName.charAt(0).toUpperCase() }}
</span>
@{{ authStore.session.twitchUserId }}
<span class="app-shell-account-label">@{{ authStore.session.twitchUserId }}</span>
</button>
</template>
<template v-else>
@@ -116,167 +166,56 @@ const linkInactive = linkBase + 'background:transparent;color:#6f6685;font-weigh
</div>
</nav>
<!-- Page content: home is full-width, all other views get a max-width container -->
<template v-if="route.name === 'home'">
<div style="max-width:1460px;margin:0 auto;padding:16px 16px 0;box-sizing:border-box;">
<slot />
</template>
<template v-else>
<div style="max-width:1460px;margin:0 auto;padding:16px 16px 0;box-sizing:border-box;">
<slot />
</div>
</template>
</div>
<!-- Account modal -->
<Teleport to="body">
<div
v-if="accountOpen"
@click.self="accountOpen = false; deleteConfirm = false"
style="position:fixed;inset:0;z-index:300;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(40,24,70,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);"
>
<div style="position:relative;width:100%;max-width:480px;background:#fff;border-radius:24px;box-shadow:0 40px 90px rgba(40,24,70,.4);overflow:hidden;">
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 28px 18px;border-bottom:1px solid #f1ecfb;">
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Mein Profil</div>
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:24px;margin:0;color:#3f3556;">@{{ authStore.session?.twitchUserId }}</h2>
</div>
<button
@click="accountOpen = false; deleteConfirm = false"
style="width:34px;height:34px;border-radius:50%;border:none;background:#f1ecfb;color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;"
></button>
</div>
<div style="padding:24px 28px;display:flex;flex-direction:column;gap:16px;">
<div style="display:flex;align-items:center;gap:10px;padding:13px 16px;border-radius:14px;background:#f6f1fd;border:1px solid #ede4fb;">
<svg width="18" height="18" viewBox="0 0 24 24" fill="#9146FF">
<path d="M4 2L2.4 6v14.5h5V23h2.7l2.5-2.5h4L21.6 16V2H4zm15.3 13.1l-2.9 2.9h-4.4l-2.5 2.5v-2.5H6.7V3.7h12.6v11.4z"/>
<path d="M11.1 7.1h1.7v5h-1.7zM15.7 7.1h1.7v5h-1.7z"/>
</svg>
<span style="font-size:14px;color:#5f44ad;font-weight:500;">Angemeldet über Twitch</span>
</div>
<div style="padding:16px;border-radius:14px;background:#fafafa;border:1px solid #f0eafc;">
<p style="font-size:12px;font-weight:700;text-transform:uppercase;letter-spacing:1.5px;color:#a99fc0;margin:0 0 10px;">Gespeicherte Daten</p>
<div style="display:flex;flex-direction:column;gap:7px;">
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
<span style="color:#6f6685;">Twitch-ID</span>
<span style="font-weight:600;color:#3f3556;">@{{ authStore.session?.twitchUserId }}</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
<span style="color:#6f6685;">Rolle</span>
<span style="font-weight:700;color:#8b6cdb;text-transform:uppercase;font-size:11px;letter-spacing:1px;">{{ authStore.session?.role }}</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
<span style="color:#6f6685;">Einreichungen</span>
<span style="font-weight:600;color:#3f3556;">Clips, Votes, Nominierungen</span>
</div>
<div style="display:flex;justify-content:space-between;font-size:13.5px;">
<span style="color:#6f6685;">Löschfrist</span>
<span style="font-weight:600;color:#059669;">März 2027 (auto.)</span>
</div>
</div>
</div>
<button
@click="privacyOpen = true"
style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #ede4fb;background:#f9f6ff;color:#6a4fb8;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
</svg>
Datenschutzerklärung lesen
</button>
<button
@click="doLogout"
style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #e2e8f0;background:#f8fafc;color:#64748b;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/>
<polyline points="16 17 21 12 16 7"/>
<line x1="21" y1="12" x2="9" y2="12"/>
</svg>
Abmelden
</button>
<template v-if="!deleteConfirm">
<button
@click="deleteConfirm = true"
style="display:flex;align-items:center;gap:8px;padding:12px 16px;border-radius:12px;border:1px solid #fecdd3;background:#fff5f5;color:#e11d48;font-family:'Outfit',sans-serif;font-size:13.5px;font-weight:600;cursor:pointer;text-align:left;width:100%;"
>
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="3 6 5 6 21 6"/>
<path d="M19 6l-1 14H6L5 6"/>
<path d="M10 11v6M14 11v6"/>
</svg>
Meine Daten löschen (Löschrecht Art. 17 DSGVO)
</button>
</template>
<template v-else>
<div style="padding:16px;border-radius:14px;background:#fff5f5;border:1.5px solid #fecdd3;">
<p style="font-size:13.5px;color:#9f1239;font-weight:600;margin:0 0 6px;">Alle deine Daten werden sofort gelöscht.</p>
<p style="font-size:12.5px;color:#e11d48;margin:0 0 14px;line-height:1.5;">Das umfasst deine Votes, Nominierungen und Clip-Einreichungen. Diese Aktion kann nicht rückgängig gemacht werden.</p>
<div style="display:flex;gap:8px;">
<button @click="deleteConfirm = false" style="flex:1;padding:10px;border-radius:10px;border:1px solid #e6dcf6;background:white;color:#6f6685;font-family:'Outfit',sans-serif;font-weight:600;font-size:13px;cursor:pointer;">Abbrechen</button>
<button @click="doLogout" style="flex:1;padding:10px;border-radius:10px;border:none;background:#e11d48;color:white;font-family:'Outfit',sans-serif;font-weight:700;font-size:13px;cursor:pointer;">Jetzt löschen</button>
</div>
</div>
</template>
</div>
</div>
</div>
</Teleport>
<!-- Privacy modal -->
<Teleport to="body">
<div
v-if="privacyOpen"
@click.self="privacyOpen = false"
style="position:fixed;inset:0;z-index:400;display:flex;align-items:center;justify-content:center;padding:24px;background:rgba(40,24,70,.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);"
>
<div style="position:relative;width:100%;max-width:680px;max-height:88vh;overflow:hidden;display:flex;flex-direction:column;background:#fff;border-radius:24px;box-shadow:0 40px 90px rgba(40,24,70,.4);">
<div style="display:flex;align-items:center;justify-content:space-between;padding:24px 32px 18px;border-bottom:1px solid #f1ecfb;flex:none;">
<div>
<div style="font-size:11px;font-weight:700;letter-spacing:2px;text-transform:uppercase;color:#8b6cdb;margin-bottom:4px;">Rechtliches</div>
<h2 style="font-family:'Cormorant Garamond',serif;font-weight:700;font-size:26px;margin:0;color:#3f3556;">Datenschutzerklärung</h2>
</div>
<button @click="privacyOpen = false" style="width:34px;height:34px;border-radius:50%;border:none;background:#f1ecfb;color:#8b6cdb;font-size:18px;cursor:pointer;display:flex;align-items:center;justify-content:center;"></button>
</div>
<div style="overflow-y:auto;padding:28px 32px;flex:1;display:flex;flex-direction:column;gap:22px;font-family:'Outfit',sans-serif;font-size:14px;line-height:1.65;color:#6f6685;">
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Verantwortliche:r</h3>
<p style="margin:0;">VTuber Star Awards, vertreten durch Jayuhime. Kontakt: datenschutz@vtuber-star-awards.de</p>
</div>
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Welche Daten wir verarbeiten</h3>
<p style="margin:0 0 8px;">Bei Teilnahme (Voting, Nominierung, Clip-Einreichung) verarbeiten wir ausschließlich deine <strong style="color:#3f3556;">Twitch-User-ID</strong> (ein technischer Bezeichner, kein Klarname) sowie den Zeitstempel deiner Aktion.</p>
<p style="margin:0;">Für Show-Erinnerungen speichern wir optional deine E-Mail-Adresse.</p>
</div>
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Rechtsgrundlage</h3>
<p style="margin:0;">Verarbeitung auf Basis von <strong style="color:#3f3556;">Art. 6 Abs. 1 lit. b DSGVO</strong> (Vertragserfüllung / vorvertragliche Maßnahmen) deine Teilnahme ist freiwillig. Für E-Mail-Erinnerungen gilt Art. 6 Abs. 1 lit. a DSGVO (Einwilligung).</p>
</div>
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Zweck der Verarbeitung</h3>
<p style="margin:0;">Durchführung des VTuber Star Awards: Sicherstellung fairer Abstimmung (1 Stimme / Person), Spam-Prävention, Admin-Review von Clip-Einreichungen.</p>
</div>
<div style="background:#fef9c3;border:1px solid #fef08a;border-radius:12px;padding:14px 16px;">
<h3 style="font-size:15px;font-weight:700;color:#854d0e;margin:0 0 6px;">Löschfristen</h3>
<p style="margin:0;color:#92400e;">Alle Teilnahmedaten (Twitch-IDs, Votes, Nominierungen, Clips) werden <strong>spätestens 6 Monate nach der Award-Show</strong> (d.&nbsp;h. bis März 2027) automatisch gelöscht. E-Mail-Adressen werden nach der Show sofort gelöscht.</p>
</div>
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Deine Rechte (Art. 1522 DSGVO)</h3>
<ul style="margin:0;padding-left:18px;display:flex;flex-direction:column;gap:5px;">
<li><strong style="color:#3f3556;">Auskunft</strong> du kannst jederzeit erfragen, welche Daten wir über dich gespeichert haben.</li>
<li><strong style="color:#3f3556;">Löschung</strong> du kannst die sofortige Löschung deiner Daten verlangen. Nutze den Button in deinem Profil oder schreibe uns.</li>
<li><strong style="color:#3f3556;">Widerspruch</strong> du kannst der Verarbeitung jederzeit widersprechen.</li>
<li><strong style="color:#3f3556;">Portabilität</strong> du kannst eine maschinenlesbare Kopie deiner Daten anfordern.</li>
<li><strong style="color:#3f3556;">Beschwerde</strong> du hast das Recht, dich bei der zuständigen Aufsichtsbehörde zu beschweren (z.&nbsp;B. LfDI Baden-Württemberg).</li>
</ul>
</div>
<div>
<h3 style="font-size:15px;font-weight:700;color:#3f3556;margin:0 0 8px;">Weitergabe an Dritte</h3>
<p style="margin:0;">Keine Weitergabe an Dritte zu Werbezwecken. Technischer Hosting-Anbieter (EU-basiert) verarbeitet Daten als Auftragsverarbeiter gemäß Art. 28 DSGVO.</p>
</div>
<p style="font-size:12px;color:#a99fc0;margin:0;">Stand: Juni 2026 · Änderungen werden auf dieser Seite bekannt gegeben.</p>
</div>
</div>
</div>
</Teleport>
<AppShellAccountModals
:account-open="accountOpen"
:privacy-open="privacyOpen"
:delete-confirm="deleteConfirm"
:session="authStore.session"
:privacy-content-blocks="privacyContentBlocks"
:privacy-email="privacyEmail"
:account-action-error="accountActionError"
:auth-loading="authStore.loading"
@close-account="closeAccountModal"
@close-privacy="closePrivacyModal"
@open-privacy="openPrivacyModal"
@request-delete="requestAccountDeletion"
@cancel-delete="cancelAccountDeletion"
@logout="doLogout"
@confirm-delete="deleteMyData"
/>
</template>
</div>
</template>
<style scoped>
@media (max-width: 720px) {
.app-shell-topbar__inner {
gap: 8px !important;
padding: 0 12px !important;
}
.app-shell-brand__text,
.app-shell-account-label {
display: none !important;
}
.app-shell-links {
gap: 2px !important;
}
.app-shell-account-button {
padding: 8px !important;
}
}
@media (max-width: 480px) {
.app-shell-link--secondary {
display: none !important;
}
}
</style>