Skip to content
Open
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
9,986 changes: 2,505 additions & 7,481 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

14 changes: 10 additions & 4 deletions src/app/components/courses/CourseCard.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { memo } from 'react';
import { Play } from 'lucide-react';
import Link from 'next/link';
import Image from 'next/image';

interface CourseCardProps {
title: string;
Expand All @@ -11,7 +13,7 @@ interface CourseCardProps {
courseHref: string;
}

export default function CourseCard({
function CourseCard({
title = 'Web3 UX Design Principles',
subtitle = 'Create intuitive interfaces for decentralized applications',
author = 'Sarah Johnson',
Expand All @@ -24,11 +26,13 @@ export default function CourseCard({
<div className="group relative w-full overflow-hidden rounded-xl border border-gray- shadow-lg bg-[#262f40] transition-all duration-300 hover:shadow-xl">
<div className="relative h-48 w-full overflow-hidden bg-gray-900">
{imageUrl ? (
// eslint-disable-next-line @next/next/no-img-element
<img
<Image
src={imageUrl}
alt={title}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105"
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform duration-300 group-hover:scale-105"
loading="lazy"
/>
) : (
<div className="h-full w-full bg-gradient-to-br from-gray-800 via-gray-900 to-gray-800" />
Expand Down Expand Up @@ -71,3 +75,5 @@ export default function CourseCard({
</div>
);
}

export default memo(CourseCard);
112 changes: 112 additions & 0 deletions src/app/components/courses/VirtualizedCourseList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
'use client';

import { memo, useCallback, useMemo } from 'react';
import { FixedSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import Image from 'next/image';
import Link from 'next/link';

export interface CourseListItem {
id: string;
title: string;
subtitle: string;
instructor: string;
progress: number;
category: string;
thumbnailUrl?: string;
}

interface VirtualizedCourseListProps {
courses: CourseListItem[];
itemHeight?: number;
overscanCount?: number;
}

function CourseRow({ course, style }: { course: CourseListItem; style: React.CSSProperties }) {
return (
<div style={style} className="px-1 py-1">
<Link
href={`/courses/${course.id}`}
className="flex items-center gap-4 rounded-lg bg-[#262f40] p-3 transition-colors hover:bg-[#2f3a4f]"
>
<div className="relative h-14 w-20 flex-shrink-0 overflow-hidden rounded-md bg-gray-700">
{course.thumbnailUrl ? (
<Image
src={course.thumbnailUrl}
alt={course.title}
fill
sizes="80px"
className="object-cover"
loading="lazy"
/>
) : (
<div className="h-full w-full bg-gradient-to-br from-gray-700 to-gray-800" />
)}
</div>
<div className="min-w-0 flex-1">
<h3 className="truncate text-sm font-semibold text-white">{course.title}</h3>
<p className="truncate text-xs text-gray-400">{course.instructor}</p>
<div className="mt-1 flex items-center gap-2">
<div className="h-1.5 flex-1 overflow-hidden rounded-full bg-gray-700">
<div
className="h-full rounded-full bg-blue-500"
style={{ width: `${course.progress}%` }}
/>
</div>
<span className="text-xs text-gray-400">{course.progress}%</span>
</div>
</div>
<span className="flex-shrink-0 rounded bg-gray-700 px-2 py-0.5 text-xs text-gray-300">
{course.category}
</span>
</Link>
</div>
);
}

const MemoizedCourseRow = memo(CourseRow);

function VirtualizedCourseList({
courses,
itemHeight = 76,
overscanCount = 5,
}: VirtualizedCourseListProps) {
const Row = useCallback(
({ index, style }: { index: number; style: React.CSSProperties }) => (
<MemoizedCourseRow course={courses[index]!} style={style} />
),
[courses],
);

const itemKey = useCallback((index: number) => courses[index]!.id, [courses]);

const listHeight = useMemo(
() => Math.min(courses.length * itemHeight, 600),
[courses.length, itemHeight],
);

if (courses.length === 0) {
return (
<div className="flex h-40 items-center justify-center text-gray-400">No courses found.</div>
);
}

return (
<AutoSizer disableHeight>
{({ width }: { width: number }) => (
<List
height={listHeight}
width={width}
itemCount={courses.length}
itemSize={itemHeight}
overscanCount={overscanCount}
itemKey={itemKey}
>
{Row}
</List>
)}
</AutoSizer>
);
}

export default memo(VirtualizedCourseList);
26 changes: 26 additions & 0 deletions src/app/components/home/CTASection.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import Link from 'next/link';

export function CTASection({ t }: { t: (key: string) => string }) {
return (
<div className="px-4 py-16 md:px-8">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-6">{t('home.ctaTitle')}</h2>
<p className="text-xl text-gray-300 mb-8">{t('home.ctaSubtitle')}</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/signup"
className="px-8 py-4 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold text-lg transition-colors"
>
{t('home.ctaPrimary')}
</Link>
<Link
href="/login"
className="px-8 py-4 bg-gray-700 hover:bg-gray-600 rounded-lg font-semibold text-lg transition-colors"
>
{t('home.ctaSecondary')}
</Link>
</div>
</div>
</div>
);
}
18 changes: 18 additions & 0 deletions src/app/components/home/CourseGridSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export function CourseGridSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 xl:gap-8 animate-pulse">
{Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="rounded-xl bg-[#262f40] overflow-hidden">
<div className="h-48 bg-gray-700" />
<div className="p-5 space-y-3">
<div className="h-5 bg-gray-700 rounded w-3/4" />
<div className="h-4 bg-gray-700 rounded w-1/2" />
<div className="h-4 bg-gray-700 rounded w-full" />
<div className="h-2 bg-gray-700 rounded-full w-full" />
<div className="h-10 bg-gray-700 rounded-lg w-full" />
</div>
</div>
))}
</div>
);
}
30 changes: 30 additions & 0 deletions src/app/components/home/FeaturedCoursesGrid.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use client';

import CourseCard from '../courses/CourseCard';

interface FeaturedCourseData {
title: string;
subtitle: string;
author: string;
progress: number;
timeRemaining: string;
imageUrl?: string;
courseHref: string;
}

export function FeaturedCoursesGrid({ courses }: { courses: FeaturedCourseData[] }) {
return (
<div
className="
grid grid-cols-1
md:grid-cols-2
lg:grid-cols-3
gap-6 xl:gap-8
"
>
{courses.map((course) => (
<CourseCard key={course.courseHref} {...course} />
))}
</div>
);
}
109 changes: 22 additions & 87 deletions src/app/components/home/HomeContent.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,25 @@
'use client';

import dynamic from 'next/dynamic';
import Link from 'next/link';
import { useInternationalization } from '@/hooks/useInternationalization';
import CourseCard from '../courses/CourseCard';
import { FeaturedCoursesGrid } from './FeaturedCoursesGrid';
import { CourseGridSkeleton } from './CourseGridSkeleton';
import type { CourseEntry } from '@/lib/course-config/types';

const WhyTeachLinkSection = dynamic(
() => import('./WhyTeachLinkSection').then((m) => ({ default: m.WhyTeachLinkSection })),
{
loading: () => (
<div className="px-4 py-16 md:px-8 bg-gray-800/30">
<div className="max-w-7xl mx-auto h-64 animate-pulse bg-gray-800 rounded-xl" />
</div>
),
},
);

const CTASection = dynamic(() => import('./CTASection').then((m) => ({ default: m.CTASection })));

interface FeaturedCourseData {
title: string;
subtitle: string;
Expand Down Expand Up @@ -96,7 +111,7 @@ export default function HomeContent({ featuredCourses }: HomeContentProps) {
href="/dashboard"
className="bg-gray-800/50 backdrop-blur-sm border border-gray-700 rounded-xl p-6 hover:bg-gray-700/50 transition-all duration-300 hover:scale-105"
>
<div className="text-3xl mb-3">🖥️</div>
<div className="text-3xl mb-3">&#x1F5A5;&#xFE0F;</div>
<h3 className="text-xl font-semibold mb-2">{t('home.navDesktopTitle')}</h3>
<p className="text-gray-400">{t('home.navDesktopDescription')}</p>
</Link>
Expand All @@ -105,7 +120,7 @@ export default function HomeContent({ featuredCourses }: HomeContentProps) {
href="/mobile-app"
className="bg-gray-800/50 backdrop-blur-sm border border-gray-700 rounded-xl p-6 hover:bg-gray-700/50 transition-all duration-300 hover:scale-105"
>
<div className="text-3xl mb-3">📱</div>
<div className="text-3xl mb-3">&#x1F4F1;</div>
<h3 className="text-xl font-semibold mb-2">{t('home.navMobileTitle')}</h3>
<p className="text-gray-400">{t('home.navMobileDescription')}</p>
</Link>
Expand All @@ -114,7 +129,7 @@ export default function HomeContent({ featuredCourses }: HomeContentProps) {
href="/courses/1"
className="bg-gray-800/50 backdrop-blur-sm border border-gray-700 rounded-xl p-6 hover:bg-gray-700/50 transition-all duration-300 hover:scale-105"
>
<div className="text-3xl mb-3">📚</div>
<div className="text-3xl mb-3">&#x1F4DA;</div>
<h3 className="text-xl font-semibold mb-2">{t('home.navCatalogTitle')}</h3>
<p className="text-gray-400">{t('home.navCatalogDescription')}</p>
</Link>
Expand All @@ -126,93 +141,13 @@ export default function HomeContent({ featuredCourses }: HomeContentProps) {
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl font-bold mb-8 text-center">{t('home.featuredTitle')}</h2>

<div
className="
grid grid-cols-1
md:grid-cols-2
lg:grid-cols-3
gap-6 xl:gap-8
"
>
<CourseCard
title="Web3 UX Design Principles"
subtitle="Create intuitive interfaces for decentralized applications"
author="Sarah Johnson"
progress={68}
timeRemaining="12h remaining"
courseHref="/courses/web3-ux-design"
imageUrl="https://thumbs.dreamstime.com/b/matrix-style-digital-rain-green-binary-code-falling-downward-direction-abstract-background-depicting-effect-stream-397887374.jpg"
/>

<CourseCard
title="Smart Contract Security Best Practices"
subtitle="Learn to secure your Cairo smart contracts against vulnerabilities"
author="Michael Chen"
progress={45}
timeRemaining="12h remaining"
courseHref="/courses/smart-contract-security"
imageUrl="https://static.vecteezy.com/system/resources/previews/053/715/379/non_2x/abstract-green-digital-rain-with-matrix-code-in-futuristic-cyber-background-perfect-for-technology-and-data-themed-visuals-png.png"
/>

<CourseCard
title="Scaling DAPps on Starknet"
subtitle="Techniques for building scalable decentralized applications"
author="Alex Rivera"
progress={12}
timeRemaining="12h remaining"
courseHref="/courses/scaling-dapps-starknet"
imageUrl="https://thumbs.dreamstime.com/b/futuristic-laptop-glowing-digital-waves-emerging-screen-dark-setting-399809314.jpg"
/>
</div>
<FeaturedCoursesGrid courses={courses} />
</div>
</div>

<div className="px-4 py-16 md:px-8 bg-gray-800/30">
<div className="max-w-7xl mx-auto">
<h2 className="text-3xl font-bold mb-12 text-center">{t('home.whyTitle')}</h2>
<WhyTeachLinkSection t={t} />

<div className="grid grid-cols-1 md:grid-cols-3 gap-8">
<div className="text-center p-6">
<div className="text-4xl mb-4">🌐</div>
<h3 className="text-xl font-semibold mb-3">{t('home.featureOfflineTitle')}</h3>
<p className="text-gray-400">{t('home.featureOfflineDescription')}</p>
</div>

<div className="text-center p-6">
<div className="text-4xl mb-4">⚡</div>
<h3 className="text-xl font-semibold mb-3">{t('home.featureCrossPlatformTitle')}</h3>
<p className="text-gray-400">{t('home.featureCrossPlatformDescription')}</p>
</div>

<div className="text-center p-6">
<div className="text-4xl mb-4">🔒</div>
<h3 className="text-xl font-semibold mb-3">{t('home.featureWeb3Title')}</h3>
<p className="text-gray-400">{t('home.featureWeb3Description')}</p>
</div>
</div>
</div>
</div>

<div className="px-4 py-16 md:px-8">
<div className="max-w-4xl mx-auto text-center">
<h2 className="text-3xl md:text-4xl font-bold mb-6">{t('home.ctaTitle')}</h2>
<p className="text-xl text-gray-300 mb-8">{t('home.ctaSubtitle')}</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/signup"
className="px-8 py-4 bg-blue-600 hover:bg-blue-700 rounded-lg font-semibold text-lg transition-colors"
>
{t('home.ctaPrimary')}
</Link>
<Link
href="/login"
className="px-8 py-4 bg-gray-700 hover:bg-gray-600 rounded-lg font-semibold text-lg transition-colors"
>
{t('home.ctaSecondary')}
</Link>
</div>
</div>
</div>
<CTASection t={t} />
</div>
);
}
Loading
Loading