Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 127 additions & 2 deletions src/app/courses/[courseId]/loading.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,130 @@
import { ServerlessPageSkeleton } from '@/components/ui/ServerlessSkeleton';
import { Skeleton } from '@/components/ui/Skeleton';

export default function CourseDetailLoading() {
return <ServerlessPageSkeleton variant="courses" label="Loading course details" />;
return (
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
{/* Hero Section Skeleton */}
<div className="relative w-full bg-linear-to-br from-[#0066FF] via-[#00C2FF] to-[#0066FF] dark:from-[#0052CC] dark:via-[#0080CC] dark:to-[#0052CC]">
<div className="h-[300px] sm:h-[400px] lg:h-[500px] w-full relative overflow-hidden">
<Skeleton className="h-full w-full" animation="wave" />
<div className="absolute inset-0 bg-linear-to-t from-black/70 via-black/40 to-transparent" />
</div>
<div className="absolute inset-0 flex items-end">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 pb-8 lg:pb-12">
<div className="max-w-4xl space-y-4">
<Skeleton height={48} width="75%" className="bg-white/20" animation="wave" />
<Skeleton
height={24}
width="100%"
className="bg-white/20 max-w-2xl"
animation="wave"
/>
<div className="flex gap-4">
<Skeleton
height={32}
width={80}
className="bg-white/10 rounded-full"
animation="wave"
/>
<Skeleton
height={32}
width={120}
className="bg-white/10 rounded-full"
animation="wave"
/>
<Skeleton
height={32}
width={100}
className="bg-white/10 rounded-full"
animation="wave"
/>
</div>
</div>
</div>
</div>
</div>

{/* Content Skeleton */}
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
{/* Left Column */}
<div className="lg:col-span-8 space-y-6">
{/* Progress Skeleton */}
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6">
<Skeleton height={24} width={200} className="mb-4" animation="wave" />
<Skeleton height={12} width="100%" className="mb-2" animation="wave" />
<div className="flex gap-4 mt-4">
<Skeleton height={32} width={120} animation="wave" />
<Skeleton height={32} width={120} animation="wave" />
</div>
</div>

{/* Video Preview Skeleton */}
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6 lg:p-8">
<Skeleton height={28} width={200} className="mb-4" animation="wave" />
<Skeleton height={300} width="100%" className="rounded-lg" animation="wave" />
</div>

{/* Syllabus Skeleton */}
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6 lg:p-8">
<Skeleton height={28} width={150} className="mb-6" animation="wave" />
<div className="space-y-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="space-y-2">
<Skeleton height={20} width="80%" animation="wave" />
<Skeleton height={16} width="60%" animation="wave" />
</div>
))}
</div>
</div>

{/* Reviews Skeleton */}
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6 lg:p-8">
<Skeleton height={28} width={150} className="mb-6" animation="wave" />
<div className="space-y-4">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="space-y-2">
<div className="flex gap-3">
<Skeleton height={40} width={40} variant="circle" animation="wave" />
<div className="flex-1 space-y-2">
<Skeleton height={16} width={120} animation="wave" />
<Skeleton height={16} width="100%" animation="wave" />
</div>
</div>
</div>
))}
</div>
</div>

{/* Instructor Skeleton */}
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6 lg:p-8">
<Skeleton height={28} width={150} className="mb-6" animation="wave" />
<div className="flex gap-4">
<Skeleton height={80} width={80} variant="circle" animation="wave" />
<div className="flex-1 space-y-2">
<Skeleton height={20} width={150} animation="wave" />
<Skeleton height={16} width="100%" animation="wave" />
<Skeleton height={16} width="80%" animation="wave" />
</div>
</div>
</div>
</div>

{/* Right Column - Enrollment CTA */}
<div className="lg:col-span-4">
<div className="bg-white dark:bg-[#1E293B] rounded-xl shadow-sm border border-[#E2E8F0] dark:border-[#334155] p-6 sticky top-4">
<Skeleton height={32} width="100%" className="mb-4" animation="wave" />
<Skeleton height={24} width="80%" className="mb-6" animation="wave" />
<div className="space-y-3 mb-6">
<Skeleton height={16} width="100%" animation="wave" />
<Skeleton height={16} width="90%" animation="wave" />
<Skeleton height={16} width="85%" animation="wave" />
</div>
<Skeleton height={48} width="100%" className="rounded-lg" animation="wave" />
</div>
</div>
</div>
</div>
</div>
);
}
37 changes: 26 additions & 11 deletions src/app/courses/[courseId]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,48 @@
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import CoursePageContent from '../components/CoursePageContent';
import { getCourseById } from '@/lib/course-config';
import type { CourseEntry } from '@/lib/course-config';

interface CoursePageProps {
params: Promise<{ courseId: string }>;
}

export async function generateMetadata({ params }: CoursePageProps): Promise<Metadata> {
await params;
// In a real app, you would fetch course data here
const { courseId } = await params;
const course = getCourseById(courseId);

if (!course) {
return {
title: 'Course Not Found | TeachLink',
};
}

return {
title: 'Course Details | TeachLink',
description:
'View detailed information about this course, including syllabus, instructor details, and enrollment options.',
title: `${course.title} | TeachLink`,
description: course.description,
openGraph: {
title: 'Course Details | TeachLink',
description: 'View course syllabus, instructor details, and enrollment options.',
title: `${course.title} | TeachLink`,
description: course.description,
type: 'website',
siteName: 'TeachLink',
},
twitter: {
card: 'summary_large_image',
site: '@teachlink',
title: 'Course Details | TeachLink',
description: 'View course syllabus, instructor details, and enrollment options.',
title: `${course.title} | TeachLink`,
description: course.description,
},
};
}

export default async function CoursePage({ params }: CoursePageProps) {
await params;
return <CoursePageContent />;
const { courseId } = await params;
const course = getCourseById(courseId);

if (!course) {
notFound();
}

return <CoursePageContent course={course} />;
}
20 changes: 13 additions & 7 deletions src/app/courses/components/CoursePageContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,21 +8,27 @@ import CourseProgress from '@/components/courses/CourseProgress';
import VideoPreview from '@/components/courses/VideoPreview';
import CourseReviews from '@/components/courses/CourseReviews';
import { useInternationalization } from '@/hooks/useInternationalization';
import type { CourseEntry } from '@/lib/course-config';

export default function CoursePageContent() {
interface CoursePageContentProps {
course: CourseEntry;
}

export default function CoursePageContent({ course }: CoursePageContentProps) {
const { t } = useInternationalization();
const isEnrolled = true;
const isEnrolled = course.progress > 0;
const completedLessons = Math.floor((course.progress / 100) * course.totalLessons);

return (
<div className="min-h-screen bg-[#F8FAFC] dark:bg-[#0F172A]">
<CourseHero />
<CourseHero course={course} />
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8 lg:py-12">
<div className="grid grid-cols-1 lg:grid-cols-12 gap-6 lg:gap-8">
<div className="lg:col-span-8 space-y-6">
<CourseProgress
progress={68}
completedLessons={14}
totalLessons={20}
progress={course.progress}
completedLessons={completedLessons}
totalLessons={course.totalLessons}
isEnrolled={isEnrolled}
/>

Expand All @@ -38,7 +44,7 @@ export default function CoursePageContent() {
<InstructorBio />
</div>
<div className="lg:col-span-4">
<EnrollmentCTA courseId="1" courseTitle="Web3 UX Design Principles" />
<EnrollmentCTA courseId={course.id} courseTitle={course.title} />
</div>
</div>
</div>
Expand Down
23 changes: 9 additions & 14 deletions src/components/courses/CourseHero.tsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,17 @@
import Image from 'next/image';
import type { CourseEntry } from '@/lib/course-config';

interface CourseHeroProps {
title?: string;
description?: string;
imageUrl?: string;
instructor?: string;
rating?: number;
enrolledStudents?: number;
course: CourseEntry;
}

export default function CourseHero({
title = 'Advanced Web Development',
description = 'Master modern web development with this comprehensive course covering frontend and backend technologies.',
imageUrl = 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?q=80&w=2071&auto=format&fit=crop',
instructor = 'John Doe',
rating = 4.8,
enrolledStudents = 1234,
}: CourseHeroProps) {
export default function CourseHero({ course }: CourseHeroProps) {
const title = course.title;
const description = course.description;
const imageUrl = course.thumbnailUrl || 'https://images.unsplash.com/photo-1522202176988-66273c2fd55f?q=80&w=2071&auto=format&fit=crop';
const instructor = course.instructor;
const rating = 4.8;
const enrolledStudents = 1234;
return (
<div className="relative w-full bg-linear-to-br from-[#0066FF] via-[#00C2FF] to-[#0066FF] dark:from-[#0052CC] dark:via-[#0080CC] dark:to-[#0052CC]">
<div className="h-[300px] sm:h-[400px] lg:h-[500px] w-full relative overflow-hidden">
Expand Down
2 changes: 2 additions & 0 deletions src/lib/course-config/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
createDefaultCourseListConfig,
} from './types';

export type { CourseEntry, CourseListConfig };

let currentConfig: CourseListConfig | null = null;

export function getCourseListConfig(): CourseListConfig {
Expand Down
Loading