419 lines
9.3 KiB
Vue
419 lines
9.3 KiB
Vue
<script setup>
|
||
import { computed, reactive, ref } from 'vue'
|
||
import FilmSuche from './FilmSuche.vue'
|
||
import { posterUrl } from '../images'
|
||
import PersonName from './PersonName.vue'
|
||
|
||
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', {
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ name: neuerName.value }),
|
||
})
|
||
if (resp.ok) neuerName.value = ''
|
||
else fehler.value = 'Der Name ist leer oder schon vergeben.'
|
||
}
|
||
|
||
async function entfernen(user) {
|
||
await fetch(`/api/users/${user.id}`, { method: 'DELETE' })
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<div class="verwaltung-tab">
|
||
<section>
|
||
<h3>{{ host ? 'Benutzer' : 'Dein Zugang' }}</h3>
|
||
|
||
<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"
|
||
placeholder="Name hinzufügen"
|
||
maxlength="40"
|
||
@keyup.enter="anlegen"
|
||
/>
|
||
<button type="button" @click="anlegen">Hinzufügen</button>
|
||
</div>
|
||
<p v-if="fehler" class="fehler">{{ fehler }}</p>
|
||
|
||
<ul v-if="liste.length" class="liste">
|
||
<li v-for="u in liste" :key="u.id">
|
||
<div class="zeile">
|
||
<span class="name"><PersonName :name="u.name" :level="u.level" /></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: 34rem;
|
||
}
|
||
|
||
h3 {
|
||
margin-bottom: 0.8rem;
|
||
color: var(--muted);
|
||
font-size: 0.78rem;
|
||
font-weight: 600;
|
||
letter-spacing: 0.09em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.anlegen {
|
||
display: flex;
|
||
gap: 0.4rem;
|
||
margin-bottom: 1.25rem;
|
||
}
|
||
|
||
.anlegen input {
|
||
flex: 1;
|
||
padding: 0.55rem 0.8rem;
|
||
font-size: 0.92rem;
|
||
}
|
||
|
||
.anlegen button {
|
||
padding: 0.55rem 1rem;
|
||
font-size: 0.88rem;
|
||
font-weight: 600;
|
||
background: var(--bg-soft);
|
||
border: 1px solid var(--line);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.anlegen button:hover {
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.liste {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.3rem;
|
||
list-style: none;
|
||
}
|
||
|
||
.liste li {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
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;
|
||
}
|
||
|
||
.weg {
|
||
padding: 0.15rem 0.4rem;
|
||
color: var(--muted);
|
||
font-size: 1.1rem;
|
||
line-height: 1;
|
||
}
|
||
|
||
.weg:hover {
|
||
color: var(--accent);
|
||
}
|
||
|
||
.leer,
|
||
.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>
|