diff options
Diffstat (limited to 'src/lib/components/VideoCard.svelte')
| -rw-r--r-- | src/lib/components/VideoCard.svelte | 133 |
1 files changed, 133 insertions, 0 deletions
diff --git a/src/lib/components/VideoCard.svelte b/src/lib/components/VideoCard.svelte new file mode 100644 index 0000000..61bf975 --- /dev/null +++ b/src/lib/components/VideoCard.svelte @@ -0,0 +1,133 @@ +<script lang="ts"> + import { formatDuration, formatViews, getBestThumbnail, type VideoThumbnail } from '$lib/api/youtube'; + + interface Props { + videoId: string; + title: string; + author: string; + authorId: string; + thumbnails: VideoThumbnail[]; + viewCount: number; + publishedText: string; + lengthSeconds: number; + } + + let { + videoId, + title, + author, + authorId, + thumbnails, + viewCount, + publishedText, + lengthSeconds + }: Props = $props(); + + const thumbnail = $derived(getBestThumbnail(thumbnails)); + const duration = $derived(formatDuration(lengthSeconds)); + const views = $derived(formatViews(viewCount)); +</script> + +<a href="/watch/{videoId}" class="video-card"> + <div class="thumbnail"> + <img src={thumbnail} alt={title} loading="lazy" /> + <span class="duration">{duration}</span> + </div> + <div class="info"> + <h3 class="title">{title}</h3> + <span + class="author" + role="link" + tabindex="0" + onclick={(e) => { e.preventDefault(); e.stopPropagation(); window.location.href = `/channel/${authorId}`; }} + onkeydown={(e) => { if (e.key === 'Enter') { e.preventDefault(); window.location.href = `/channel/${authorId}`; } }} + > + {author} + </span> + <div class="meta"> + <span>{views}</span> + <span class="separator">•</span> + <span>{publishedText}</span> + </div> + </div> +</a> + +<style> + .video-card { + display: block; + text-decoration: none; + color: inherit; + border-radius: 8px; + overflow: hidden; + transition: transform 0.15s ease; + } + + .video-card:hover { + transform: translateY(-2px); + } + + .thumbnail { + position: relative; + aspect-ratio: 16 / 9; + background: var(--bg-secondary); + border-radius: 8px; + overflow: hidden; + } + + .thumbnail img { + width: 100%; + height: 100%; + object-fit: cover; + } + + .duration { + position: absolute; + bottom: 8px; + right: 8px; + background: rgba(0, 0, 0, 0.8); + color: #fff; + padding: 2px 6px; + border-radius: 4px; + font-size: 0.8rem; + font-weight: 500; + } + + .info { + padding: 0.75rem 0; + } + + .title { + font-size: 0.95rem; + font-weight: 500; + line-height: 1.3; + margin: 0 0 0.5rem; + display: -webkit-box; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + color: var(--text-primary); + } + + .author { + font-size: 0.85rem; + color: var(--text-secondary); + text-decoration: none; + display: block; + margin-bottom: 0.25rem; + cursor: pointer; + } + + .author:hover { + color: var(--text-primary); + } + + .meta { + font-size: 0.8rem; + color: var(--text-muted); + } + + .separator { + margin: 0 0.25rem; + } +</style> |
