update
This commit is contained in:
1 parent
db77c1f985
commit
a90044b0a7
21 files changed
+1111
-253
No files matched your search
@@ -0,0 +1,360 @@
|
||||
<script setup>
|
||||
// Nach Leuten suchen: einen Namen tippen, eine Person wählen, dann
|
||||
// stehen ihre Filme da — getrennt nach gespielt und Regie geführt.
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import MovieCard from './MovieCard.vue'
|
||||
import { posterUrl } from '../images'
|
||||
import { passtZurLeiste } from '../filter.js'
|
||||
|
||||
const props = defineProps({
|
||||
filter: { type: Object, default: () => ({}) },
|
||||
ansicht: { type: String, default: 'kompakt' },
|
||||
gesehenIds: { type: Set, default: () => new Set() },
|
||||
ohneGesehene: { type: Boolean, default: true },
|
||||
// Kommt aus der Filmansicht: mit dieser Person soll es losgehen
|
||||
start: { type: Object, default: null },
|
||||
})
|
||||
|
||||
const emit = defineEmits(['open', 'gestartet'])
|
||||
|
||||
const query = ref('')
|
||||
const personen = ref([])
|
||||
const personenLaden = ref(false)
|
||||
const gewaehlt = ref(null)
|
||||
const rollen = ref({ gespielt: [], regie: [] })
|
||||
const laden = ref(false)
|
||||
const fehler = ref('')
|
||||
|
||||
let timer = null
|
||||
let laufende = 0
|
||||
let laufendeWahl = 0
|
||||
|
||||
async function suchen(term) {
|
||||
if (term.trim().length < 2) {
|
||||
personen.value = []
|
||||
return
|
||||
}
|
||||
const meine = ++laufende
|
||||
personenLaden.value = true
|
||||
try {
|
||||
const resp = await fetch(`/api/personen?q=${encodeURIComponent(term.trim())}`)
|
||||
if (!resp.ok) throw new Error('Suche fehlgeschlagen')
|
||||
const daten = await resp.json()
|
||||
if (meine !== laufende) return
|
||||
personen.value = daten
|
||||
} catch {
|
||||
if (meine === laufende) personen.value = []
|
||||
} finally {
|
||||
if (meine === laufende) personenLaden.value = false
|
||||
}
|
||||
}
|
||||
|
||||
watch(query, () => {
|
||||
clearTimeout(timer)
|
||||
timer = setTimeout(() => suchen(query.value), 350)
|
||||
})
|
||||
|
||||
async function waehlen(person) {
|
||||
const meine = ++laufendeWahl
|
||||
gewaehlt.value = person
|
||||
query.value = ''
|
||||
personen.value = []
|
||||
laden.value = true
|
||||
fehler.value = ''
|
||||
try {
|
||||
const resp = await fetch(`/api/personen/${person.id}/filme`)
|
||||
if (!resp.ok) throw new Error('Das hat nicht geklappt.')
|
||||
const daten = await resp.json()
|
||||
if (meine !== laufendeWahl) return
|
||||
rollen.value = daten
|
||||
} catch (err) {
|
||||
if (meine === laufendeWahl) fehler.value = err.message
|
||||
} finally {
|
||||
if (meine === laufendeWahl) laden.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function passt(movie) {
|
||||
if (props.ohneGesehene && props.gesehenIds.has(movie.id)) return false
|
||||
return passtZurLeiste(movie, props.filter)
|
||||
}
|
||||
|
||||
// Die Reihenfolge aus der Leiste. "Wie auf IMDb" ist hier die von
|
||||
// TMDB: die bekanntesten Filme zuerst.
|
||||
const wuerfel = new Map()
|
||||
function zufall(movie) {
|
||||
if (!wuerfel.has(movie.id)) wuerfel.set(movie.id, Math.random())
|
||||
return wuerfel.get(movie.id)
|
||||
}
|
||||
|
||||
const SORTS = {
|
||||
random: (a, b) => zufall(a) - zufall(b),
|
||||
rating: (a, b) => (b.vote_average || 0) - (a.vote_average || 0),
|
||||
newest: (a, b) => (b.year || 0) - (a.year || 0),
|
||||
oldest: (a, b) => (a.year || 9999) - (b.year || 9999),
|
||||
title: (a, b) => a.title.localeCompare(b.title, 'de'),
|
||||
}
|
||||
|
||||
function sortiert(liste) {
|
||||
const sort = SORTS[props.filter.sort]
|
||||
return sort ? [...liste].sort(sort) : liste
|
||||
}
|
||||
|
||||
const abschnitte = computed(() =>
|
||||
[
|
||||
['Als Schauspieler', rollen.value.gespielt],
|
||||
['Als Regisseur', rollen.value.regie],
|
||||
]
|
||||
.map(([titel, liste]) => [titel, sortiert(liste.filter(passt))])
|
||||
.filter(([, liste]) => liste.length),
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.start,
|
||||
(person) => {
|
||||
if (!person) return
|
||||
waehlen(person)
|
||||
emit('gestartet')
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
async function oeffnen(movie) {
|
||||
const resp = await fetch(`/api/search/${movie.id}`)
|
||||
if (resp.ok) emit('open', await resp.json())
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="personen-tab">
|
||||
<div class="suchzeile">
|
||||
<input
|
||||
v-model="query"
|
||||
type="search"
|
||||
class="big-search"
|
||||
placeholder="Wen suchst du?"
|
||||
autofocus
|
||||
/>
|
||||
<button v-if="query" type="button" class="leeren" title="Suche leeren" @click="query = ''">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Tippt jemand, stehen die Leute vorn, bis einer gewählt ist -->
|
||||
<template v-if="query.trim().length >= 2">
|
||||
<p v-if="!personen.length && !personenLaden" class="note">Nichts gefunden.</p>
|
||||
<ul v-else class="leute">
|
||||
<li v-for="p in personen" :key="p.id">
|
||||
<button type="button" @click="waehlen(p)">
|
||||
<img v-if="p.profile_path" :src="posterUrl(p.profile_path, 'w185')" :alt="p.name" />
|
||||
<span v-else class="kein-bild"></span>
|
||||
<span class="text">
|
||||
<strong>{{ p.name }}</strong>
|
||||
<span class="fach">{{ p.fach }}</span>
|
||||
<span v-if="p.bekannt_fuer.length" class="bekannt">
|
||||
{{ p.bekannt_fuer.join(' · ') }}
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
|
||||
<template v-else-if="gewaehlt">
|
||||
<div class="kopf">
|
||||
<img
|
||||
v-if="gewaehlt.profile_path"
|
||||
:src="posterUrl(gewaehlt.profile_path, 'w185')"
|
||||
:alt="gewaehlt.name"
|
||||
/>
|
||||
<div>
|
||||
<h3>{{ gewaehlt.name }}</h3>
|
||||
<p class="fach">{{ gewaehlt.fach }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="laden" class="note">Suche läuft…</p>
|
||||
<p v-else-if="fehler" class="note error">{{ fehler }}</p>
|
||||
<p v-else-if="!abschnitte.length" class="note">Nichts gefunden. Stell die Filter weiter.</p>
|
||||
|
||||
<section v-for="[titel, liste] in abschnitte" :key="titel">
|
||||
<p class="label">
|
||||
{{ titel }}
|
||||
<span class="anzahl">{{ liste.length }}</span>
|
||||
</p>
|
||||
<div class="grid" :class="ansicht">
|
||||
<MovieCard
|
||||
v-for="film in liste"
|
||||
:key="film.id"
|
||||
:movie="film"
|
||||
:ansicht="ansicht"
|
||||
:gesehen="gesehenIds.has(film.id)"
|
||||
@open="oeffnen"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.personen-tab {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.suchzeile {
|
||||
position: relative;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.big-search {
|
||||
width: 100%;
|
||||
padding: 0.9rem 2.6rem 0.9rem 1.1rem;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.leeren {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
right: 0.9rem;
|
||||
color: var(--muted);
|
||||
font-size: 1.4rem;
|
||||
line-height: 1;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
.leeren:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* Die Trefferliste: Foto, Name, Fach und wofür man die Person kennt */
|
||||
.leute {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
gap: 0.7rem;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.leute button {
|
||||
display: flex;
|
||||
gap: 0.8rem;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0.6rem;
|
||||
text-align: left;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.leute button:hover {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.leute img,
|
||||
.kein-bild {
|
||||
flex-shrink: 0;
|
||||
width: 56px;
|
||||
height: 84px;
|
||||
object-fit: cover;
|
||||
background: #1c1c22;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
.text {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
}
|
||||
|
||||
.fach,
|
||||
.bekannt {
|
||||
color: var(--muted);
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.bekannt {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kopf {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.kopf img {
|
||||
width: 64px;
|
||||
height: 96px;
|
||||
object-fit: cover;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.kopf h3 {
|
||||
font-size: 1.3rem;
|
||||
}
|
||||
|
||||
section {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
gap: 0.45rem;
|
||||
align-items: center;
|
||||
margin-bottom: 1.1rem;
|
||||
color: var(--muted);
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.09em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.anzahl {
|
||||
padding: 0.1rem 0.45rem;
|
||||
letter-spacing: 0;
|
||||
background: var(--bg-soft);
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(190px, 1fr));
|
||||
gap: 1.1rem;
|
||||
}
|
||||
|
||||
.grid.details {
|
||||
grid-template-columns: repeat(auto-fill, min(calc(50% - 0.55rem), calc(4 * 190px + 3 * 1.1rem)));
|
||||
}
|
||||
|
||||
.note {
|
||||
padding: 2rem 0;
|
||||
color: var(--muted);
|
||||
font-size: 0.95rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.note.error {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(105px, 1fr));
|
||||
gap: 0.6rem;
|
||||
}
|
||||
|
||||
.grid.details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.leute {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user