Skip to content

Commit d099cdf

Browse files
committed
Unify YouTube previews across library resource cards
1 parent 522bcf4 commit d099cdf

File tree

5 files changed

+66
-5
lines changed

5 files changed

+66
-5
lines changed

src/app/dashboard/pipeline-course-card.tsx

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Link from 'next/link';
22
import Image from 'next/image';
33
import { Play, BookOpen } from 'lucide-react';
44
import type { Language } from '@/lib/translations';
5-
import { extractYoutubeId } from '@/lib/utils/youtube';
5+
import { getYoutubeThumbnailUrl } from '@/lib/utils/youtube';
66

77
export interface PipelineCourseData {
88
id: string;
@@ -46,10 +46,7 @@ export function PipelineCourseCard({
4646
course: PipelineCourseData;
4747
language: Language;
4848
}) {
49-
const videoId = extractYoutubeId(course.url);
50-
const thumbnailUrl = videoId
51-
? `https://img.youtube.com/vi/${videoId}/mqdefault.jpg`
52-
: null;
49+
const thumbnailUrl = getYoutubeThumbnailUrl(course.url);
5350

5451
const completedCount = course.progress?.completedSections?.length ?? 0;
5552
const progressPercent = course.sectionCount > 0 ? (completedCount / course.sectionCount) * 100 : 0;

src/app/library/resource-card.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Link from 'next/link';
66
import { t, type Language } from '@/lib/translations';
77
import { QuickQuiz } from '@/components/quick-quiz';
88
import { CornerBrackets } from '@/components/ui/corner-brackets';
9+
import { ResourceVideoPreview } from './resource-video-preview';
910

1011
const RESOURCE_TYPE_ICONS: Record<string, string> = {
1112
youtube: '▶',
@@ -151,6 +152,13 @@ export function ResourceCard({ resource, isLoggedIn, language }: ResourceCardPro
151152
</span>
152153
</div>
153154

155+
<ResourceVideoPreview
156+
url={resource.url}
157+
title={resource.title}
158+
animateOnHover={!isLocked}
159+
className="mb-4"
160+
/>
161+
154162
{/* Title */}
155163
<h3 className={`text-lg font-medium leading-tight mb-1 ${
156164
isLocked ? 'text-j-text-tertiary' : 'text-j-text'
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import Image from 'next/image';
2+
import { getYoutubeThumbnailUrl } from '@/lib/utils/youtube';
3+
4+
interface ResourceVideoPreviewProps {
5+
url?: string | null;
6+
title: string;
7+
className?: string;
8+
animateOnHover?: boolean;
9+
}
10+
11+
export function ResourceVideoPreview({
12+
url,
13+
title,
14+
className = '',
15+
animateOnHover = true,
16+
}: ResourceVideoPreviewProps) {
17+
const thumbnailUrl = getYoutubeThumbnailUrl(url);
18+
19+
if (!thumbnailUrl) return null;
20+
21+
return (
22+
<div
23+
className={`relative aspect-video overflow-hidden border border-j-border bg-j-bg-alt/50 ${className}`.trim()}
24+
>
25+
<Image
26+
src={thumbnailUrl}
27+
alt={title}
28+
fill
29+
sizes="(max-width: 640px) 100vw, (max-width: 1024px) 50vw, 33vw"
30+
className={`object-cover transition-transform duration-300 ${
31+
animateOnHover ? 'group-hover:scale-105' : ''
32+
}`}
33+
/>
34+
<div className="pointer-events-none absolute inset-0 bg-gradient-to-t from-black/45 via-transparent to-transparent" />
35+
<span className="absolute left-2 top-2 border border-white/20 bg-black/60 px-1.5 py-0.5 font-mono text-[9px] tracking-[0.12em] uppercase text-white">
36+
YouTube
37+
</span>
38+
</div>
39+
);
40+
}

src/app/library/user-resource-card.tsx

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import Link from 'next/link';
44
import { CornerBrackets } from '@/components/ui/corner-brackets';
55
import type { Language } from '@/lib/translations';
6+
import { ResourceVideoPreview } from './resource-video-preview';
67

78
interface UserResource {
89
id: string;
@@ -61,6 +62,12 @@ export function UserResourceCard({ resource, language }: UserResourceCardProps)
6162
</span>
6263
</div>
6364

65+
<ResourceVideoPreview
66+
url={resource.url}
67+
title={resource.title}
68+
className="mb-4"
69+
/>
70+
6471
{/* Icon + Title */}
6572
<div className="flex items-start gap-2 mb-3">
6673
<span className="text-lg shrink-0">{icon}</span>

src/lib/utils/youtube.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,12 @@ export function extractYoutubeId(url: string | null): string | null {
1414
export function isValidYoutubeUrl(url: string): boolean {
1515
return YOUTUBE_REGEX.test(url);
1616
}
17+
18+
export function getYoutubeThumbnailUrl(
19+
url: string | null | undefined,
20+
quality: 'default' | 'mqdefault' | 'hqdefault' | 'sddefault' | 'maxresdefault' = 'mqdefault'
21+
): string | null {
22+
const videoId = extractYoutubeId(url ?? null);
23+
if (!videoId) return null;
24+
return `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
25+
}

0 commit comments

Comments
 (0)