Files
horror/frontend/src/components/MovieCard.vue
2026-09-16 17:55:23 +02:00

307 lines
6.5 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup>
import { computed } from 'vue'
import { backdropUrl, posterUrl } from '../images'
const props = defineProps({
movie: { type: Object, required: true },
aufDemRad: { type: Boolean, default: false },
// Schon gelaufen — die Karte tritt dann zurück
gesehen: { 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, gesehen }]"
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>
/* Schon gelaufen — die Karte tritt zurück, bleibt aber lesbar.
Beim Überfahren kommt sie zurück. */
.card.gesehen {
opacity: 0.45;
filter: grayscale(1);
}
.card.gesehen:hover {
opacity: 1;
filter: none;
}
/* 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;
}
/* Schmal und kompakt: nur Poster, Titel und Jahr */
@media (max-width: 640px) {
.card.kompakt .body {
gap: 0.2rem;
padding: 0.5rem;
}
.card.kompakt h2 {
-webkit-line-clamp: 2;
line-clamp: 2;
min-height: calc(2 * 1.25em);
font-size: 0.82rem;
line-height: 1.25;
}
.card.kompakt .meta {
font-size: 0.72rem;
}
.card.kompakt .teaser {
display: none;
}
.rating {
font-size: 0.72rem;
}
}
/* Schmal: Cover und Text nebeneinander, die Bilder in einer Reihe darunter */
@media (max-width: 700px) {
.card.details {
grid-template-columns: 110px minmax(0, 1fr);
grid-template-rows: auto auto;
}
.card.details .poster {
width: 110px;
}
.card.details .shots {
grid-column: 1 / -1;
grid-template-columns: repeat(4, 1fr);
}
}
</style>