260 lines
5.6 KiB
Vue
260 lines
5.6 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 },
|
||
// 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;
|
||
}
|
||
|
||
.card.gesehen:hover {
|
||
opacity: 1;
|
||
}
|
||
|
||
/* 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>
|