Files
archy/packages/app/src/components/content/TVSeriesCard.vue
T
DorianandClaude Opus 4.6 2304e5904e fix(app): fix image fallbacks across all content grids, refactor into useContentImages
- Create useContentImages<T> composable eliminating ~240 lines of duplicated
  image loading/fallback logic across 6 grid components
- Fix book covers: use full fetchBookImage chain (Open Library → Google Books → Wikipedia)
- Fix TV series images: try disambiguated Wikipedia title first (e.g. "Chernobyl (TV series)")
- Add Wikipedia image fetching for places (fetchPlaceImage)
- Rewrite all SVG fallbacks to consistent text-only style (no icons)
- Add generateWebsiteFallback for NewsGrid websites variant
- Fix song extraction regex catching raw song_ext: prefix in titles
- Fix player bar: clean song_ext: prefix from display, mute error text
- Fix Code panel: auto-load projects on mount when list is empty
- Improve chat bubble spacing (py-2.5) and first message top margin (pt-6)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 15:55:40 +00:00

108 lines
3.9 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.
<template>
<button
class="flex gap-3 p-2 rounded-xl transition-all duration-200 text-left w-full group overflow-hidden"
:class="isDark
? 'hover:bg-white/5 active:bg-white/10'
: 'hover:bg-black/[0.03] active:bg-black/5'"
@click="$emit('select', series)"
>
<div class="poster-card-sm shrink-0 w-12 aspect-[2/3] rounded-lg overflow-hidden">
<img
v-if="posterSrc"
:src="posterSrc"
:alt="series.title"
class="w-full h-full object-cover rounded-[6px] transition-transform duration-300 group-hover:scale-105"
loading="lazy"
@error="posterFailed = true"
/>
<div
v-else
class="w-full h-full rounded-[6px] bg-cover bg-center"
:style="{ backgroundImage: `url(${fallbackPoster})` }"
/>
</div>
<div class="min-w-0 flex-1 py-0.5">
<p class="text-sm font-semibold truncate"
:class="isDark ? 'text-white/90' : 'text-gray-900'">{{ series.title }}</p>
<p class="text-xs mt-0.5"
:class="isDark ? 'text-white/40' : 'text-gray-500'">
{{ yearDisplay }}<template v-if="series.network"> · {{ series.network }}</template>
</p>
<p v-if="series.synopsis"
class="text-xs mt-0.5 line-clamp-2"
:class="isDark ? 'text-white/35' : 'text-gray-400'">
{{ series.synopsis }}
</p>
<div class="flex items-center gap-1.5 mt-1.5">
<span v-if="series.rating && series.rating > 0"
class="text-xs font-semibold px-1.5 py-0.5 rounded"
:class="ratingClass">
{{ series.rating.toFixed(1) }}
</span>
<span v-if="series.seasons"
class="text-xs px-1.5 py-0.5 rounded font-medium"
:class="isDark ? 'bg-white/8 text-white/50' : 'bg-black/5 text-gray-500'">
{{ series.seasons }}S
</span>
<span v-if="series.status === 'ongoing'"
class="text-xs px-1.5 py-0.5 rounded font-medium"
:class="isDark ? 'bg-success/15 text-success/70' : 'bg-green-50 text-green-600'">
ongoing
</span>
<span v-else-if="series.status === 'ended'"
class="text-xs px-1.5 py-0.5 rounded font-medium"
:class="isDark ? 'bg-white/8 text-white/40' : 'bg-black/5 text-gray-400'">
ended
</span>
</div>
</div>
</button>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import type { TVSeries } from '@aiui/core/types/content'
import { useTheme } from '@/composables/useTheme'
import { generateTVSeriesFallback, fetchTVImage } from '@/composables/useImageFallback'
const props = defineProps<{ series: TVSeries }>()
defineEmits<{ select: [series: TVSeries] }>()
const { isDark } = useTheme()
const posterFailed = ref(false)
const fetchedPoster = ref<string | null>(null)
const posterSrc = computed(() => {
if (posterFailed.value) return null
return props.series.posterUrl || fetchedPoster.value
})
const fallbackPoster = computed(() =>
generateTVSeriesFallback(props.series.title, props.series.year)
)
const yearDisplay = computed(() => {
if (!props.series.year) return ''
if (props.series.endYear && props.series.endYear !== props.series.year) {
return `${props.series.year}${props.series.endYear}`
}
if (props.series.status === 'ongoing') return `${props.series.year}`
return String(props.series.year)
})
const ratingClass = computed(() => {
const r = props.series.rating ?? 0
if (r >= 8.5) return isDark.value ? 'bg-success/20 text-success' : 'bg-success/10 text-green-700'
if (r >= 7.5) return isDark.value ? 'bg-accent/20 text-accent' : 'bg-accent/10 text-amber-700'
return isDark.value ? 'bg-white/10 text-white/50' : 'bg-black/5 text-gray-500'
})
onMounted(() => {
if (props.series.posterUrl) return
fetchTVImage(props.series.title, props.series.year).then((result) => {
if (result.posterUrl) fetchedPoster.value = result.posterUrl
})
})
</script>