352 lines
14 KiB
TypeScript
352 lines
14 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useEffect, use } from 'react';
|
|
import Link from 'next/link';
|
|
import { notFound } from 'next/navigation';
|
|
import { getLessonBySlug, getLessons } from '@/lib/actions/lessonActions';
|
|
import { VideoPlayer } from '@/components/VideoPlayer';
|
|
import { CodeBlock } from '@/components/CodeBlock';
|
|
import { ResourceCard } from '@/components/ResourceCard';
|
|
import {
|
|
FileText,
|
|
Code,
|
|
Download,
|
|
Clock,
|
|
FileArchive,
|
|
FileSpreadsheet,
|
|
ArrowLeft,
|
|
Share2,
|
|
Check,
|
|
Eye,
|
|
Calendar,
|
|
Sparkles,
|
|
ExternalLink,
|
|
Loader2
|
|
} from 'lucide-react';
|
|
import { YoutubeIcon as Youtube } from '@/components/icons/YoutubeIcon';
|
|
import { GithubIcon as Github } from '@/components/icons/BrandIcons';
|
|
|
|
export default function LessonDetailPage({
|
|
params
|
|
}: {
|
|
params: Promise<{ locale: string; slug: string }>;
|
|
}) {
|
|
const { locale, slug } = use(params);
|
|
const [activeTab, setActiveTab] = useState<'code' | 'notes' | 'downloads' | 'chapters'>('code');
|
|
const [copiedLink, setCopiedLink] = useState(false);
|
|
|
|
const [lesson, setLesson] = useState<any>(null);
|
|
const [relatedLessons, setRelatedLessons] = useState<any[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function loadData() {
|
|
setLoading(true);
|
|
const data = await getLessonBySlug(slug);
|
|
if (data) {
|
|
setLesson(data);
|
|
|
|
// Auto-select first non-empty tab
|
|
const hasCode = data.codeSnippets && data.codeSnippets.length > 0;
|
|
const hasNotes = data.notesMarkdown && data.notesMarkdown.length > 0;
|
|
const hasDownloads = data.downloads && data.downloads.length > 0;
|
|
const hasChapters = data.chapters && data.chapters.length > 0;
|
|
|
|
if (hasCode) setActiveTab('code');
|
|
else if (hasNotes) setActiveTab('notes');
|
|
else if (hasDownloads) setActiveTab('downloads');
|
|
else if (hasChapters) setActiveTab('chapters');
|
|
|
|
const all = await getLessons();
|
|
setRelatedLessons(all.filter((v: any) => v.id !== data.id).slice(0, 3));
|
|
}
|
|
setLoading(false);
|
|
}
|
|
loadData();
|
|
}, [slug]);
|
|
|
|
if (!loading && !lesson) {
|
|
notFound();
|
|
}
|
|
|
|
if (loading || !lesson) {
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 py-24 text-center text-slate-400 flex flex-col items-center justify-center gap-3">
|
|
<Loader2 className="w-8 h-8 animate-spin text-red-500" />
|
|
<p className="text-sm font-semibold">Loading tutorial...</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const hasCode = lesson.codeSnippets && lesson.codeSnippets.length > 0;
|
|
const hasNotes = lesson.notesMarkdown && lesson.notesMarkdown.length > 0;
|
|
const hasDownloads = lesson.downloads && lesson.downloads.length > 0;
|
|
const hasChapters = lesson.chapters && lesson.chapters.length > 0;
|
|
const hasAnyResources = hasCode || hasNotes || hasDownloads || hasChapters;
|
|
|
|
const handleShare = async () => {
|
|
try {
|
|
await navigator.clipboard.writeText(window.location.href);
|
|
setCopiedLink(true);
|
|
setTimeout(() => setCopiedLink(false), 2000);
|
|
} catch (err) {
|
|
console.error('Share error:', err);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-10 space-y-10 flex-1 w-full">
|
|
|
|
{/* Breadcrumb & Navigation Back */}
|
|
<div className="flex flex-wrap items-center justify-between gap-4">
|
|
<div className="flex items-center gap-2 text-xs text-slate-400">
|
|
<Link href={`/${locale}`} className="hover:text-white transition-colors">Home</Link>
|
|
<span>/</span>
|
|
<Link href={`/${locale}/lessons`} className="hover:text-white transition-colors">Lessons</Link>
|
|
<span>/</span>
|
|
<span className="text-slate-200 font-semibold truncate max-w-xs">{lesson.title}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
onClick={handleShare}
|
|
className="flex items-center gap-1.5 px-3 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 text-xs font-semibold border border-slate-800 transition-colors"
|
|
>
|
|
{copiedLink ? (
|
|
<>
|
|
<Check className="w-3.5 h-3.5 text-emerald-400" />
|
|
<span className="text-emerald-400">Link Copied</span>
|
|
</>
|
|
) : (
|
|
<>
|
|
<Share2 className="w-3.5 h-3.5 text-slate-400" />
|
|
<span>Share Page</span>
|
|
</>
|
|
)}
|
|
</button>
|
|
|
|
<Link
|
|
href={`/${locale}/lessons`}
|
|
className="flex items-center gap-1 px-3 py-1.5 rounded-xl bg-slate-900 hover:bg-slate-800 text-slate-300 text-xs font-semibold border border-slate-800 transition-colors"
|
|
>
|
|
<ArrowLeft className="w-3.5 h-3.5" />
|
|
<span>Back to Lessons</span>
|
|
</Link>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Main Header Info */}
|
|
<div className="space-y-4">
|
|
<div className="flex flex-wrap items-center gap-2">
|
|
<span className="px-3 py-1 rounded-full bg-red-500/10 text-red-400 text-xs font-bold border border-red-500/20">
|
|
{lesson.category}
|
|
</span>
|
|
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
|
<Calendar className="w-3.5 h-3.5 text-slate-500" />
|
|
{new Date(lesson.publishDate).toLocaleDateString()}
|
|
</span>
|
|
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
|
<Eye className="w-3.5 h-3.5 text-slate-500" />
|
|
{lesson.viewsCount.toLocaleString()} Views
|
|
</span>
|
|
{hasDownloads && (
|
|
<span className="flex items-center gap-1 text-xs text-slate-400 bg-slate-900 px-3 py-1 rounded-full border border-slate-800">
|
|
<Download className="w-3.5 h-3.5 text-slate-500" />
|
|
{lesson.downloadCount.toLocaleString()} Downloads
|
|
</span>
|
|
)}
|
|
</div>
|
|
|
|
<h1 className="text-2xl sm:text-4xl font-black text-white tracking-tight leading-tight">
|
|
{lesson.title}
|
|
</h1>
|
|
|
|
<p className="text-sm text-slate-300 max-w-3xl leading-relaxed">
|
|
{lesson.summary}
|
|
</p>
|
|
</div>
|
|
|
|
{/* Embedded Video Player */}
|
|
<VideoPlayer
|
|
youtubeId={lesson.youtubeId}
|
|
youtubeUrl={lesson.youtubeUrl}
|
|
title={lesson.title}
|
|
chapters={lesson.chapters}
|
|
/>
|
|
|
|
{/* Resource Tabs (Shown ONLY if resources exist) */}
|
|
{hasAnyResources && (
|
|
<div className="space-y-6">
|
|
|
|
{/* Tab Header Buttons */}
|
|
<div className="flex items-center gap-2 border-b border-slate-800 pb-2 overflow-x-auto scrollbar-none">
|
|
{hasCode && (
|
|
<button
|
|
onClick={() => setActiveTab('code')}
|
|
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
|
activeTab === 'code'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
|
}`}
|
|
>
|
|
<Code className="w-4 h-4" />
|
|
<span>Code Blocks ({lesson.codeSnippets.length} Files)</span>
|
|
</button>
|
|
)}
|
|
|
|
{hasNotes && (
|
|
<button
|
|
onClick={() => setActiveTab('notes')}
|
|
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
|
activeTab === 'notes'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
|
}`}
|
|
>
|
|
<FileText className="w-4 h-4" />
|
|
<span>Lesson Notes & Summary</span>
|
|
</button>
|
|
)}
|
|
|
|
{hasDownloads && (
|
|
<button
|
|
onClick={() => setActiveTab('downloads')}
|
|
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
|
activeTab === 'downloads'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
|
}`}
|
|
>
|
|
<Download className="w-4 h-4" />
|
|
<span>Downloads & Links ({lesson.downloads.length})</span>
|
|
</button>
|
|
)}
|
|
|
|
{hasChapters && (
|
|
<button
|
|
onClick={() => setActiveTab('chapters')}
|
|
className={`flex items-center gap-2 px-5 py-3 rounded-2xl text-xs font-bold transition-all whitespace-nowrap ${
|
|
activeTab === 'chapters'
|
|
? 'bg-red-600 text-white shadow-lg shadow-red-600/30'
|
|
: 'bg-slate-900 hover:bg-slate-800 text-slate-400 hover:text-white border border-slate-800'
|
|
}`}
|
|
>
|
|
<Clock className="w-4 h-4" />
|
|
<span>Video Chapters ({lesson.chapters.length})</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
|
|
{/* Tab Contents */}
|
|
<div>
|
|
{/* TAB 1: CODE BLOCKS */}
|
|
{activeTab === 'code' && hasCode && (
|
|
<div className="space-y-4">
|
|
<div className="p-4 rounded-xl bg-red-950/20 border border-red-800/30 text-xs text-red-300 flex items-center justify-between">
|
|
<span>💡 You can inspect all code snippets by clicking file tabs and copy them directly into your project using "Copy Code".</span>
|
|
</div>
|
|
|
|
<CodeBlock snippets={lesson.codeSnippets} />
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 2: LESSON NOTES */}
|
|
{activeTab === 'notes' && hasNotes && (
|
|
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-4">
|
|
<h3 className="font-extrabold text-white text-lg flex items-center gap-2">
|
|
<FileText className="w-5 h-5 text-red-400" />
|
|
<span>Key Lesson Notes & Takeaways</span>
|
|
</h3>
|
|
|
|
<div className="space-y-3">
|
|
{lesson.notesMarkdown.map((note: string, idx: number) => (
|
|
<div key={idx} className="p-4 rounded-xl bg-slate-950 border border-slate-800 text-xs text-slate-200 leading-relaxed">
|
|
{note}
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 3: DOWNLOADS & LINKS */}
|
|
{activeTab === 'downloads' && hasDownloads && (
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
|
|
{lesson.downloads.map((item: any, idx: number) => (
|
|
<div key={idx} className="p-5 rounded-2xl bg-slate-900/80 border border-slate-800 flex items-center justify-between gap-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="w-10 h-10 rounded-xl bg-slate-800 flex items-center justify-center text-red-400 flex-shrink-0">
|
|
{item.type === 'zip' && <FileArchive className="w-5 h-5" />}
|
|
{item.type === 'github' && <Github className="w-5 h-5 text-white" />}
|
|
{item.type === 'pdf' && <FileSpreadsheet className="w-5 h-5 text-emerald-400" />}
|
|
{item.type === 'link' && <ExternalLink className="w-5 h-5 text-sky-400" />}
|
|
</div>
|
|
<div>
|
|
<h4 className="font-bold text-white text-xs">{item.title}</h4>
|
|
{item.size && <p className="text-[11px] text-slate-500 font-mono">Size: {item.size}</p>}
|
|
</div>
|
|
</div>
|
|
|
|
<a
|
|
href={item.url}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="px-4 py-2 rounded-xl bg-red-600 hover:bg-red-500 text-white text-xs font-bold flex items-center gap-1.5 transition-colors shadow-md shadow-red-600/20"
|
|
>
|
|
<Download className="w-3.5 h-3.5" />
|
|
<span>Download / Open</span>
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
|
|
{/* TAB 4: CHAPTERS */}
|
|
{activeTab === 'chapters' && hasChapters && (
|
|
<div className="p-6 rounded-2xl bg-slate-900/60 border border-slate-800 space-y-3">
|
|
<h3 className="font-bold text-white text-sm mb-4">Timestamped Video Index</h3>
|
|
<div className="space-y-2">
|
|
{lesson.chapters.map((chap: any) => (
|
|
<div key={chap.time} className="p-3 rounded-xl bg-slate-950 border border-slate-800 flex items-center justify-between text-xs">
|
|
<div className="flex items-center gap-3">
|
|
<span className="font-mono text-red-400 font-bold bg-slate-900 px-2 py-1 rounded border border-slate-800">
|
|
{chap.time}
|
|
</span>
|
|
<span className="text-white font-medium">{chap.title}</span>
|
|
</div>
|
|
<a
|
|
href={`${lesson.youtubeUrl}&t=${chap.seconds}s`}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
className="text-xs text-red-400 hover:underline flex items-center gap-1"
|
|
>
|
|
<Youtube className="w-3.5 h-3.5" />
|
|
<span>Watch</span>
|
|
</a>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
</div>
|
|
)}
|
|
|
|
{/* Related YouTube Videos */}
|
|
<div className="pt-10 border-t border-slate-800 space-y-6">
|
|
<h3 className="text-xl font-extrabold text-white flex items-center gap-2">
|
|
<Sparkles className="w-5 h-5 text-red-500" />
|
|
<span>Other Popular Video Tutorials</span>
|
|
</h3>
|
|
|
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
{relatedLessons.map((rel) => (
|
|
<ResourceCard key={rel.id} lesson={rel} locale={locale} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
);
|
|
}
|