Benutzer, Sammlungen, Wünsche und zwei Kartenansichten
Die Runde ist jetzt benannt. Der Host pflegt die Namensliste im neuen Reiter "Verwaltung", alle anderen wählen sich oben rechts einen aus. Der Name landet serverseitig an Vorschlägen und Wertungen, statt aus einem Textfeld zu kommen, in das jeder schreiben konnte, was er wollte. Dazu zwei Sammelstellen, beide nach Person gruppiert und für alle sichtbar, aber nur selbst bearbeitbar: - "Sammlungen": Filme, die man sehen möchte. Getrennt vom Rad-Vorschlag, von dem es weiterhin einen je Person gibt. - "Wünsche": was der Seite noch fehlt. Der Host darf hier alle Einträge ändern und löschen. Die Filmliste hat jetzt zwei Ansichten. Kompakt ist die gewohnte Karte, Details stellt Cover, Text und vier Szenenbilder nebeneinander — eine Karte so breit wie vier kompakte oder die halbe Reihe, je nachdem was zuerst zutrifft. Reihen und Einzelfilme sind dabei exakt gleich hoch; die Blätterleiste liegt dafür über dem Cover statt darunter. Die Filmansicht nutzt die Breite: links Cover, Text und Knöpfe, rechts der Trailer, darunter die Bilder. Ein Klick zeigt ein Bild in voller Größe. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013XVnRop5c3or3B8xE2hpgZ
This commit is contained in:
@@ -3,7 +3,11 @@ import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import { abonnieren } from './live'
|
||||
import MovieCard from './components/MovieCard.vue'
|
||||
import FilterSidebar from './components/FilterSidebar.vue'
|
||||
import MerklisteTab from './components/MerklisteTab.vue'
|
||||
import MovieDetail from './components/MovieDetail.vue'
|
||||
import NamensWahl from './components/NamensWahl.vue'
|
||||
import VerwaltungTab from './components/VerwaltungTab.vue'
|
||||
import WuenscheTab from './components/WuenscheTab.vue'
|
||||
import SearchTab from './components/SearchTab.vue'
|
||||
import StreamTab from './components/StreamTab.vue'
|
||||
import WatchedTab from './components/WatchedTab.vue'
|
||||
@@ -16,10 +20,19 @@ const loading = ref(true)
|
||||
const error = ref('')
|
||||
const search = ref('')
|
||||
const sort = ref('random')
|
||||
|
||||
// Kompakt zeigt viele Karten nebeneinander, Details dafür mehr je Film
|
||||
const ANSICHT_SCHLUESSEL = 'filmabend_ansicht'
|
||||
const ansicht = ref(localStorage.getItem(ANSICHT_SCHLUESSEL) || 'kompakt')
|
||||
|
||||
watch(ansicht, (wert) => localStorage.setItem(ANSICHT_SCHLUESSEL, wert))
|
||||
const selected = ref(null)
|
||||
// Nach dem Neuladen soll derselbe Reiter offen sein
|
||||
const TAB_SCHLUESSEL = 'filmabend_tab'
|
||||
const TAB_IDS = ['netflix', 'canon', 'search', 'wheel', 'watched', 'stream']
|
||||
const TAB_IDS = [
|
||||
'netflix', 'canon', 'search', 'wheel', 'merkliste', 'watched', 'wuensche',
|
||||
'verwaltung', 'stream',
|
||||
]
|
||||
const gemerkterTab = localStorage.getItem(TAB_SCHLUESSEL)
|
||||
const activeTab = ref(TAB_IDS.includes(gemerkterTab) ? gemerkterTab : 'netflix')
|
||||
|
||||
@@ -200,9 +213,14 @@ const tabs = computed(() => [
|
||||
{ id: 'netflix', label: 'Netflix', count: netflixMovies.value.length },
|
||||
{ id: 'canon', label: 'Horror-Kanon', count: canonGroups.value.length },
|
||||
{ id: 'search', label: 'Suche', count: null },
|
||||
...(host.value
|
||||
? []
|
||||
: [{ id: 'merkliste', label: 'Sammlungen', count: null }]),
|
||||
{ id: 'wheel', label: 'Rad', count: null },
|
||||
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
|
||||
{ id: 'stream', label: 'Stream', count: null },
|
||||
{ id: 'watched', label: 'Gesehen', count: gesehenZahl.value },
|
||||
{ id: 'wuensche', label: 'Wünsche', count: wuensche.value.length },
|
||||
...(host.value ? [{ id: 'verwaltung', label: 'Verwaltung', count: null }] : []),
|
||||
])
|
||||
|
||||
const rad = ref(null)
|
||||
@@ -227,16 +245,43 @@ const gesehenZahl = computed(
|
||||
|
||||
// Wer den Abend leitet, darf drehen und die Runde leeren. Kein Login —
|
||||
// die Tastenkombination reicht, die Seite steht ohnehin nur Freunden offen.
|
||||
// Ein Name für alles: Vorschläge und Wertungen. Ohne Namen läuft es
|
||||
// anonym, das ist ausdrücklich erlaubt.
|
||||
const NAME_SCHLUESSEL = 'filmabend_name'
|
||||
const name = ref(localStorage.getItem(NAME_SCHLUESSEL) || '')
|
||||
// Die Runde: der Host pflegt die Namen, alle anderen wählen sich einen aus.
|
||||
// Den Namen setzt danach der Server an Vorschläge und Wertungen.
|
||||
const runde = ref([])
|
||||
const ich = ref(null)
|
||||
|
||||
watch(name, (wert) => localStorage.setItem(NAME_SCHLUESSEL, wert))
|
||||
async function michSetzen(id) {
|
||||
await fetch('/api/users/waehlen', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ user_id: id ?? null }),
|
||||
})
|
||||
ladeRunde()
|
||||
}
|
||||
|
||||
async function ladeRunde() {
|
||||
try {
|
||||
const resp = await fetch('/api/users')
|
||||
if (!resp.ok) return
|
||||
const daten = await resp.json()
|
||||
runde.value = daten.items
|
||||
ich.value = daten.mine
|
||||
} catch {
|
||||
/* dann eben ohne */
|
||||
}
|
||||
}
|
||||
|
||||
const HOST_SCHLUESSEL = 'filmabend_host'
|
||||
const host = ref(localStorage.getItem(HOST_SCHLUESSEL) === '1')
|
||||
|
||||
// Die Verwaltung verschwindet mit dem Hostmodus. Bleibt sie offen, sieht
|
||||
// ein Gast sie weiter, ohne dass ein Reiter markiert wäre.
|
||||
watch(host, (an) => {
|
||||
// Verwaltung sieht nur der Host, Vorschläge nur die Gäste
|
||||
const weg = an ? 'merkliste' : 'verwaltung'
|
||||
if (activeTab.value === weg) activeTab.value = 'netflix'
|
||||
})
|
||||
|
||||
function tastatur(e) {
|
||||
// e.code statt e.key: mit drei Zusatztasten liefert das Layout sonst
|
||||
// je nach System ein anderes Zeichen
|
||||
@@ -253,6 +298,30 @@ const meinVorschlag = ref(null)
|
||||
// Alle Filme auf dem Rad, damit die Karten es zeigen können
|
||||
const aufDemRad = ref([])
|
||||
|
||||
// Die eigene Merkliste — was man selbst sehen möchte, unabhängig vom Rad
|
||||
const merkliste = ref([])
|
||||
|
||||
// Was der Seite noch fehlt
|
||||
const wuensche = ref([])
|
||||
|
||||
async function ladeWuensche() {
|
||||
try {
|
||||
const resp = await fetch('/api/features')
|
||||
if (resp.ok) wuensche.value = await resp.json()
|
||||
} catch {
|
||||
/* dann eben ohne */
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeMerkliste() {
|
||||
try {
|
||||
const resp = await fetch('/api/wishlist')
|
||||
if (resp.ok) merkliste.value = await resp.json()
|
||||
} catch {
|
||||
/* dann eben ohne */
|
||||
}
|
||||
}
|
||||
|
||||
async function ladeVorschlag() {
|
||||
try {
|
||||
const resp = await fetch('/api/suggestions')
|
||||
@@ -397,9 +466,14 @@ onMounted(() => {
|
||||
load()
|
||||
ladeVorschlag()
|
||||
ladeGesehen()
|
||||
ladeRunde()
|
||||
ladeMerkliste()
|
||||
ladeWuensche()
|
||||
// Schlägt jemand anders etwas vor, sollen es auch die Karten zeigen
|
||||
abbestellen = abonnieren((nachricht) => {
|
||||
if (nachricht.type === 'suggestions' || nachricht.type === 'reset') ladeVorschlag()
|
||||
else if (nachricht.type === 'users') ladeRunde()
|
||||
else if (nachricht.type === 'features') ladeWuensche()
|
||||
})
|
||||
window.addEventListener('keydown', tastatur)
|
||||
})
|
||||
@@ -411,7 +485,11 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="page" :class="{ breit: ['search', 'wheel', 'stream', 'watched'].includes(activeTab) }">
|
||||
<div class="page" :class="{
|
||||
breit: [
|
||||
'search', 'wheel', 'stream', 'watched', 'merkliste', 'wuensche', 'verwaltung',
|
||||
].includes(activeTab),
|
||||
}">
|
||||
<nav class="tabs">
|
||||
<button
|
||||
v-for="tab in tabs"
|
||||
@@ -423,14 +501,8 @@ onUnmounted(() => {
|
||||
{{ tab.label }}
|
||||
<span v-if="tab.count !== null" class="count">{{ tab.count }}</span>
|
||||
</button>
|
||||
<input
|
||||
v-model.trim="name"
|
||||
type="text"
|
||||
class="name"
|
||||
placeholder="Dein Name"
|
||||
maxlength="40"
|
||||
/>
|
||||
<span v-if="host" class="host">Host</span>
|
||||
<NamensWahl v-if="!host" :items="runde" :mine="ich" @waehlen="michSetzen" />
|
||||
<span v-else class="host">Host</span>
|
||||
</nav>
|
||||
|
||||
<SearchTab v-if="activeTab === 'search'" @open="selected = $event" />
|
||||
@@ -446,11 +518,28 @@ onUnmounted(() => {
|
||||
<WatchedTab
|
||||
v-else-if="activeTab === 'watched'"
|
||||
:host="host"
|
||||
:name="name"
|
||||
@open="selected = $event"
|
||||
@changed="ladeGesehen"
|
||||
/>
|
||||
|
||||
<MerklisteTab
|
||||
v-else-if="activeTab === 'merkliste'"
|
||||
:items="merkliste"
|
||||
:ich="ich"
|
||||
@open="selected = $event"
|
||||
@changed="ladeMerkliste"
|
||||
/>
|
||||
|
||||
<WuenscheTab
|
||||
v-else-if="activeTab === 'wuensche'"
|
||||
:items="wuensche"
|
||||
:ich="ich"
|
||||
:host="host"
|
||||
@changed="ladeWuensche"
|
||||
/>
|
||||
|
||||
<VerwaltungTab v-else-if="activeTab === 'verwaltung'" :items="runde" />
|
||||
|
||||
<StreamTab v-else-if="activeTab === 'stream'" />
|
||||
|
||||
<div v-else class="layout">
|
||||
@@ -464,7 +553,7 @@ onUnmounted(() => {
|
||||
Nichts gefunden{{ search ? ` für „${search}“` : '' }}.
|
||||
</p>
|
||||
|
||||
<div class="grid">
|
||||
<div class="grid" :class="ansicht">
|
||||
<template
|
||||
v-for="entry in activeTab === 'netflix' ? visible : visibleCanon"
|
||||
:key="entry.group"
|
||||
@@ -473,6 +562,7 @@ onUnmounted(() => {
|
||||
v-if="entry.parts.length === 1"
|
||||
:movie="entry.parts[0]"
|
||||
:auf-dem-rad="aufDemRad.includes(entry.parts[0].id)"
|
||||
:ansicht="ansicht"
|
||||
@open="selected = $event"
|
||||
/>
|
||||
<SeriesCard
|
||||
@@ -480,6 +570,7 @@ onUnmounted(() => {
|
||||
:parts="entry.parts"
|
||||
:start="entry.best"
|
||||
:auf-dem-rad="aufDemRad"
|
||||
:ansicht="ansicht"
|
||||
@open="selected = $event"
|
||||
/>
|
||||
</template>
|
||||
@@ -490,6 +581,7 @@ onUnmounted(() => {
|
||||
v-if="genreOptions.length"
|
||||
v-model:search="search"
|
||||
v-model:sort="sort"
|
||||
v-model:ansicht="ansicht"
|
||||
v-model:ranges="ranges"
|
||||
:regler="REGLER"
|
||||
:grenzen="grenzen"
|
||||
@@ -510,8 +602,9 @@ onUnmounted(() => {
|
||||
@open="selected = $event"
|
||||
:mein-vorschlag="meinVorschlag"
|
||||
:host="host"
|
||||
:name="name"
|
||||
:gesehen="gesehen.some((e) => e.movie_id === selected.id)"
|
||||
:gemerkt="merkliste.some((e) => e.mine && e.movie_id === selected.id)"
|
||||
@gemerkt="ladeMerkliste"
|
||||
@suggested="nachVorschlag"
|
||||
@watched="ladeGesehen"
|
||||
/>
|
||||
@@ -557,15 +650,9 @@ onUnmounted(() => {
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.tabs .name {
|
||||
width: 11rem;
|
||||
margin: 0 0 0.35rem auto;
|
||||
padding: 0.35rem 0.6rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.host {
|
||||
align-self: center;
|
||||
margin-left: 0.4rem;
|
||||
padding: 0.15rem 0.5rem;
|
||||
color: var(--accent);
|
||||
font-size: 0.7rem;
|
||||
@@ -629,6 +716,15 @@ onUnmounted(() => {
|
||||
gap: 1.1rem;
|
||||
}
|
||||
|
||||
/* Eine Detailkarte ist so breit wie vier kompakte Karten nebeneinander —
|
||||
oder die halbe Reihe, falls die schmaler ist. */
|
||||
.grid.details {
|
||||
grid-template-columns: repeat(
|
||||
auto-fill,
|
||||
min(calc(50% - 0.55rem), calc(4 * 190px + 3 * 1.1rem))
|
||||
);
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 2.5rem 0;
|
||||
color: var(--muted);
|
||||
|
||||
Reference in New Issue
Block a user