This commit is contained in:
team3
2026-09-17 15:40:28 +02:00
parent a1684cf54f
commit ea54a86db4
30 changed files with 3680 additions and 989 deletions

View File

@@ -1,13 +1,51 @@
<script setup>
import { ref } from 'vue'
import { computed, reactive, ref } from 'vue'
import FilmSuche from './FilmSuche.vue'
import { posterUrl } from '../images'
defineProps({
const props = defineProps({
items: { type: Array, default: () => [] },
// Der Host pflegt die ganze Runde, alle anderen nur den eigenen Zugang
host: Boolean,
ich: { type: Object, default: null },
})
const liste = computed(() =>
props.host ? props.items : props.items.filter((u) => u.id === props.ich?.id)
)
const neuerName = ref('')
const fehler = ref('')
// Der Anmeldefilm ist das Passwort: wer den Namen wählt, muss ihn in der
// Suche finden. Welcher es ist, gibt der Server nicht heraus — hier steht
// nur, ob überhaupt einer gesetzt ist.
const setzt = ref(null)
async function schutzSetzen(user, movieId) {
await fetch(`/api/users/${user.id}/schutz`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ movie_id: movieId }),
})
delete gezeigt[user.id]
setzt.value = null
}
// Wer den Film ändern darf, darf ihn auch nachschlagen — sonst weiß man
// nicht, was man gerade überschreibt. Sichtbar wird er erst auf Klick.
const gezeigt = reactive({})
async function schutzZeigen(user) {
if (gezeigt[user.id]) {
delete gezeigt[user.id]
return
}
gezeigt[user.id] = { laden: true }
const resp = await fetch(`/api/users/${user.id}/schutz`)
gezeigt[user.id] = resp.ok ? await resp.json() : { fehler: true }
}
async function anlegen() {
fehler.value = ''
const resp = await fetch('/api/users', {
@@ -27,9 +65,14 @@ async function entfernen(user) {
<template>
<div class="verwaltung-tab">
<section>
<h3>Benutzer</h3>
<h3>{{ host ? 'Benutzer' : 'Dein Zugang' }}</h3>
<div class="anlegen">
<p v-if="!host" class="erklaerung">
Hinterleg einen Film, dann fragt die Seite beim Wechsel auf deinen
Namen danach. Ohne Film bleibt dein Name für alle offen.
</p>
<div v-if="host" class="anlegen">
<input
v-model="neuerName"
type="text"
@@ -41,27 +84,101 @@ async function entfernen(user) {
</div>
<p v-if="fehler" class="fehler">{{ fehler }}</p>
<ul v-if="items.length" class="liste">
<li v-for="u in items" :key="u.id">
<span class="name">{{ u.name }}</span>
<button
type="button"
class="weg"
title="Aus der Runde nehmen"
@click="entfernen(u)"
>
×
</button>
<ul v-if="liste.length" class="liste">
<li v-for="u in liste" :key="u.id">
<div class="zeile">
<span class="name">{{ u.name }}</span>
<button
v-if="u.geschuetzt"
type="button"
class="auge"
:class="{ an: gezeigt[u.id] }"
:title="gezeigt[u.id] ? 'Film verbergen' : 'Hinterlegten Film zeigen'"
@click="schutzZeigen(u)"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M1 12s4-7.5 11-7.5S23 12 23 12s-4 7.5-11 7.5S1 12 1 12z" />
<circle cx="12" cy="12" r="3.2" />
<line v-if="!gezeigt[u.id]" x1="3" y1="21" x2="21" y2="3" />
</svg>
</button>
<button type="button" class="klein" @click="setzt = u">
{{ u.geschuetzt ? 'Ändern' : 'Anmeldefilm' }}
</button>
<button
v-if="u.geschuetzt"
type="button"
class="klein"
title="Namen wieder für alle öffnen"
@click="schutzSetzen(u, null)"
>
Aufheben
</button>
<button
v-if="host"
type="button"
class="weg"
title="Aus der Runde nehmen"
@click="entfernen(u)"
>
×
</button>
</div>
<p v-if="gezeigt[u.id]" class="verraten">
<template v-if="gezeigt[u.id].laden">Moment…</template>
<template v-else-if="gezeigt[u.id].film">
<img
v-if="posterUrl(gezeigt[u.id].film.poster_path, 'w92')"
:src="posterUrl(gezeigt[u.id].film.poster_path, 'w92')"
:alt="gezeigt[u.id].film.title"
/>
<span>
{{ gezeigt[u.id].film.title }}
<span v-if="gezeigt[u.id].film.year" class="jahr">
{{ gezeigt[u.id].film.year }}
</span>
</span>
</template>
<template v-else>Der Film ließ sich nicht laden.</template>
</p>
</li>
</ul>
<p v-else class="leer">Noch niemand in der Runde.</p>
</section>
<Teleport to="body">
<div v-if="setzt" class="hintergrund" @click.self="setzt = null">
<div class="fenster" role="dialog" aria-modal="true">
<p class="label">Anmeldefilm für</p>
<h2>{{ setzt.name }}</h2>
<p class="frage">
{{
setzt.id === ich?.id
? 'Diesen Film musst du beim Wechsel auf deinen Namen anklicken.'
: `Diesen Film muss ${setzt.name} beim Wechsel anklicken.`
}}
</p>
<FilmSuche
platzhalter="Film als Passwort …"
@waehlen="schutzSetzen(setzt, $event.id)"
/>
<div class="fuss">
<button type="button" class="abbrechen" @click="setzt = null">
Abbrechen
</button>
</div>
</div>
</div>
</Teleport>
</div>
</template>
<style scoped>
.verwaltung-tab {
max-width: 26rem;
max-width: 34rem;
}
h3 {
@@ -107,14 +224,66 @@ h3 {
.liste li {
display: flex;
flex-direction: column;
gap: 0.5rem;
align-items: center;
padding: 0.55rem 0.85rem;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 8px;
}
.zeile {
display: flex;
gap: 0.5rem;
align-items: center;
}
/* Der hinterlegte Film, aufgeklappt */
.verraten {
display: flex;
gap: 0.6rem;
align-items: center;
padding-top: 0.5rem;
color: var(--text);
font-size: 0.9rem;
border-top: 1px solid var(--line);
}
.verraten img {
width: 40px;
border-radius: 4px;
}
.jahr {
color: var(--muted);
font-size: 0.82rem;
}
.auge {
display: grid;
place-items: center;
width: 1.8rem;
height: 1.8rem;
color: var(--muted);
border: 1px solid var(--line);
border-radius: 6px;
}
.auge svg {
width: 1rem;
height: 1rem;
fill: none;
stroke: currentColor;
stroke-width: 1.8;
stroke-linecap: round;
}
.auge:hover,
.auge.an {
color: var(--text);
border-color: var(--accent);
}
.name {
flex: 1;
font-size: 0.95rem;
@@ -132,13 +301,117 @@ h3 {
}
.leer,
.fehler {
.fehler,
.erklaerung {
color: var(--muted);
font-size: 0.88rem;
}
.erklaerung {
margin-bottom: 1rem;
line-height: 1.5;
}
.fehler {
margin-bottom: 0.8rem;
color: var(--accent);
}
.marke {
padding: 0.1rem 0.4rem;
color: var(--muted);
font-size: 0.68rem;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
border: 1px solid var(--line);
border-radius: 4px;
}
.klein {
padding: 0.25rem 0.55rem;
color: var(--muted);
font-size: 0.78rem;
border: 1px solid var(--line);
border-radius: 6px;
}
.klein:hover {
color: var(--text);
border-color: var(--accent);
}
/* ── Fenster zum Setzen ──────────────────────────────────────────── */
.hintergrund {
position: fixed;
inset: 0;
z-index: 50;
display: grid;
place-items: center;
padding: 1rem;
background: rgba(0, 0, 0, 0.8);
}
.fenster {
display: flex;
flex-direction: column;
gap: 0.6rem;
width: min(38rem, 100%);
max-height: min(42rem, 90vh);
padding: 1.5rem 1.75rem;
background: var(--bg-soft);
border: 1px solid var(--line);
border-radius: 14px;
}
.label {
color: var(--muted);
font-size: 0.78rem;
font-weight: 600;
letter-spacing: 0.09em;
text-transform: uppercase;
}
.fenster h2 {
font-size: 1.35rem;
line-height: 1.2;
}
.frage {
color: var(--muted);
font-size: 0.9rem;
}
.fuss {
display: flex;
justify-content: flex-end;
}
.abbrechen {
padding: 0.55rem 1.1rem;
color: var(--muted);
font-size: 0.88rem;
font-weight: 600;
border: 1px solid var(--line);
border-radius: 8px;
}
.abbrechen:hover {
color: var(--text);
}
@media (max-width: 640px) {
.hintergrund {
padding: 0;
}
.fenster {
width: 100%;
height: 100%;
max-height: none;
padding: 1rem;
border: 0;
border-radius: 0;
}
}
</style>