summaryrefslogtreecommitdiff
path: root/src/lib/components/VideoCard.svelte
diff options
context:
space:
mode:
Diffstat (limited to 'src/lib/components/VideoCard.svelte')
-rw-r--r--src/lib/components/VideoCard.svelte133
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>