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
248 lines
5.4 KiB
Vue
248 lines
5.4 KiB
Vue
<script setup>
|
||
import { computed } from 'vue'
|
||
import { backdropUrl, posterUrl } from '../images'
|
||
|
||
const props = defineProps({
|
||
movie: { type: Object, required: true },
|
||
aufDemRad: { type: Boolean, default: false },
|
||
ansicht: { type: String, default: 'kompakt' },
|
||
})
|
||
|
||
defineEmits(['open'])
|
||
|
||
const poster = computed(() => posterUrl(props.movie.poster_path, 'w342'))
|
||
const rating = computed(() =>
|
||
props.movie.vote_average ? props.movie.vote_average.toFixed(1) : null
|
||
)
|
||
|
||
// Drei reichen: mehr macht die Karte hoch, ohne mehr zu zeigen
|
||
const shots = computed(() =>
|
||
(props.movie.images || []).slice(0, 4).map((pfad) => backdropUrl(pfad, 'w300'))
|
||
)
|
||
|
||
const meta = computed(() =>
|
||
[props.movie.year || 'Jahr unbekannt', props.movie.runtime && `${props.movie.runtime} Min.`]
|
||
.filter(Boolean)
|
||
.join(' · ')
|
||
)
|
||
</script>
|
||
|
||
<template>
|
||
<button
|
||
class="card"
|
||
:class="[ansicht, { 'auf-dem-rad': aufDemRad }]"
|
||
type="button"
|
||
@click="$emit('open', movie)"
|
||
>
|
||
<span v-if="aufDemRad" class="rad-marke" title="Steht auf dem Rad">Rad</span>
|
||
<div class="haupt">
|
||
<div class="poster">
|
||
<img v-if="poster" :src="poster" :alt="movie.title" loading="lazy" />
|
||
<span v-else class="placeholder">Kein Cover</span>
|
||
<span v-if="rating" class="rating">{{ rating }}</span>
|
||
</div>
|
||
<div class="body">
|
||
<h2>{{ movie.title }}</h2>
|
||
<p class="meta">{{ meta }}</p>
|
||
<template v-if="ansicht === 'details'">
|
||
<p v-if="movie.genres?.length" class="genres">
|
||
{{ movie.genres.join(' · ') }}
|
||
</p>
|
||
<p v-if="movie.vote_count" class="stimmen">
|
||
★ {{ rating }} aus {{ movie.vote_count.toLocaleString('de-DE') }} Stimmen
|
||
</p>
|
||
</template>
|
||
<p class="teaser">{{ movie.teaser || movie.overview }}</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div v-if="ansicht === 'details' && shots.length" class="shots">
|
||
<img
|
||
v-for="(shot, i) in shots"
|
||
:key="shot"
|
||
:src="shot"
|
||
:alt="`${movie.title} – Szenenbild ${i + 1}`"
|
||
loading="lazy"
|
||
/>
|
||
</div>
|
||
</button>
|
||
</template>
|
||
|
||
<style scoped>
|
||
/* Details: breite Karte, Poster links, alles lesbar statt abgeschnitten */
|
||
.haupt {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* Details: vier Spalten — Cover, Text, dann zweimal zwei Bilder.
|
||
.haupt löst sich auf, damit Cover und Text eigene Spalten sind. */
|
||
.card.details {
|
||
display: grid;
|
||
grid-template-columns: 150px minmax(180px, 1fr) 340px;
|
||
/* Feste Zeilenhöhe nach dem Cover: sonst sind Reihen höher als
|
||
Einzelfilme, weil bei ihnen die Teil-Zeile dazukommt */
|
||
grid-template-rows: 225px;
|
||
gap: 0.9rem;
|
||
align-items: stretch;
|
||
padding: 0.8rem;
|
||
}
|
||
|
||
.card.details .haupt {
|
||
display: contents;
|
||
}
|
||
|
||
.card.details .poster {
|
||
width: 150px;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* Feste 16:9 statt Streckung — sonst zieht ein langer Teaser die Bilder
|
||
in die Länge */
|
||
/* Zwei Spalten mit je zwei Bildern untereinander */
|
||
.shots {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 0.35rem;
|
||
min-width: 0;
|
||
}
|
||
|
||
.shots img {
|
||
width: 100%;
|
||
aspect-ratio: 16 / 9;
|
||
object-fit: cover;
|
||
border-radius: 6px;
|
||
}
|
||
|
||
/* Der Teaser bleibt auf vier Zeilen — sonst wird die Karte höher als die
|
||
kompakte, und die soll das Maß bleiben */
|
||
|
||
.card.details .body {
|
||
padding: 0;
|
||
}
|
||
|
||
.genres,
|
||
.stimmen {
|
||
color: var(--muted);
|
||
font-size: 0.78rem;
|
||
}
|
||
|
||
.stimmen {
|
||
color: #f5c518;
|
||
}
|
||
|
||
.card {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
text-align: left;
|
||
background: var(--bg-soft);
|
||
border: 1px solid var(--line);
|
||
border-radius: 12px;
|
||
transition: transform 0.15s ease, border-color 0.15s ease;
|
||
}
|
||
|
||
/* Steht der Film auf dem Rad, soll man das im Raster sehen */
|
||
.card.auf-dem-rad {
|
||
border-color: #6bbf7a;
|
||
box-shadow: inset 0 0 0 1px #6bbf7a;
|
||
}
|
||
|
||
.rad-marke {
|
||
position: absolute;
|
||
top: 0.5rem;
|
||
left: 0.5rem;
|
||
z-index: 1;
|
||
padding: 0.1rem 0.4rem;
|
||
color: #fff;
|
||
font-size: 0.65rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
background: #6bbf7a;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.card:hover,
|
||
.card:focus-visible {
|
||
transform: translateY(-4px);
|
||
border-color: var(--accent);
|
||
}
|
||
|
||
.poster {
|
||
position: relative;
|
||
aspect-ratio: 2 / 3;
|
||
background: #1c1c22;
|
||
}
|
||
|
||
.poster img {
|
||
display: block;
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
}
|
||
|
||
.placeholder {
|
||
display: grid;
|
||
place-items: center;
|
||
height: 100%;
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
}
|
||
|
||
.rating {
|
||
position: absolute;
|
||
top: 0.5rem;
|
||
right: 0.5rem;
|
||
padding: 0.15rem 0.45rem;
|
||
font-size: 0.8rem;
|
||
font-weight: 600;
|
||
background: rgba(0, 0, 0, 0.75);
|
||
border-radius: 6px;
|
||
}
|
||
|
||
.body {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.35rem;
|
||
padding: 0.85rem;
|
||
}
|
||
|
||
h2 {
|
||
/* Feste Höhe für drei Zeilen: sonst rutschen die Kacheln einer Reihe
|
||
unterschiedlich weit nach unten */
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 3;
|
||
line-clamp: 3;
|
||
overflow: hidden;
|
||
min-height: calc(3 * 1.3em);
|
||
font-size: 1rem;
|
||
line-height: 1.3;
|
||
}
|
||
|
||
.meta {
|
||
color: var(--muted);
|
||
font-size: 0.8rem;
|
||
}
|
||
|
||
.teaser {
|
||
display: -webkit-box;
|
||
-webkit-box-orient: vertical;
|
||
-webkit-line-clamp: 4;
|
||
line-clamp: 4;
|
||
overflow: hidden;
|
||
min-height: calc(4 * 1.45em);
|
||
color: #b9b9c6;
|
||
font-size: 0.85rem;
|
||
line-height: 1.45;
|
||
}
|
||
|
||
.card.details .body,
|
||
.card.details .shots {
|
||
overflow: hidden;
|
||
}
|
||
</style>
|