first commit
This commit is contained in:
@@ -0,0 +1,351 @@
|
||||
'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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user