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
4 changes: 2 additions & 2 deletions app/(pages)/(landingPage)/About.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import Button from "@/components/atoms/form/Button";

const About = () => {
return (
<section id="mission" className="py-16 px-4 sm:px-6 lg:px-8 bg-white/80 backdrop-blur-xl">
<section id="mission" aria-labelledby="about-heading" className="py-16 px-4 sm:px-6 lg:px-8 bg-white/80 backdrop-blur-xl">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16 items-center">
{/* Left Section - Text Content */}
<div className="space-y-6 lg:space-y-8">
<div className="space-y-4">
<h2 className="text-4xl sm:text-5xl lg:text-7xl font-bold leading-tight text-transparent bg-clip-text bg-gradient-to-r from-accent to-highlight">
<h2 id="about-heading" className="text-4xl sm:text-5xl lg:text-7xl font-bold leading-tight text-transparent bg-clip-text bg-gradient-to-r from-accent to-highlight">
Where Deen Meets{" "}
<span>Excellence</span>
</h2>
Expand Down
4 changes: 2 additions & 2 deletions app/(pages)/(landingPage)/CTA.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import Link from "next/link";

export default function CTA() {
return (
<section className="relative mx-auto py-10 px-4 sm:px-8 bg-basic text-white shadow-xl flex flex-col items-center justify-center overflow-hidden">
<h2 className="text-4xl sm:text-5xl font-bold mb-4 text-center">
<section aria-labelledby="cta-heading" className="relative mx-auto py-10 px-4 sm:px-8 bg-basic text-white shadow-xl flex flex-col items-center justify-center overflow-hidden">
<h2 id="cta-heading" className="text-4xl sm:text-5xl font-bold mb-4 text-center">
Ready to Join Deen Bridge?
</h2>
<p className="text-lg sm:text-xl mb-8 text-center max-w-2xl">
Expand Down
95 changes: 95 additions & 0 deletions app/(pages)/(landingPage)/FeaturedCourses.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import Link from "next/link";
import { ArrowRight, Sparkles } from "lucide-react";
import { fetchCourses } from "@/lib/actions/courses/fetch-courses";
import { getAverageRating } from "@/hooks/getAverageRating";
import FeaturedCoursesCarousel from "./FeaturedCoursesCarousel";
import LandingCourseCard from "./LandingCourseCard";

// Cache the rendered subtree for five minutes. `fetchCourses` uses axios
// (not the Next.js `fetch` API), so we can't tag the request itself with
// `next.revalidate`. Caching the component closes that gap and stops the
// landing page from hitting /api/courses on every visitor.
export const revalidate = 300;
Comment thread
Whiznificent marked this conversation as resolved.

function rankCourses(courses) {
if (!Array.isArray(courses) || courses.length === 0) return [];

return [...courses]
.filter((c) => c && (c._id || c.id))
.map((c) => {
const enrolled = Array.isArray(c.enrolledUsers) ? c.enrolledUsers.length : 0;
const rating = getAverageRating(c.reviews);
const reviewCount = Array.isArray(c.reviews) ? c.reviews.length : 0;
return { course: c, enrolled, rating, reviewCount };
})
.sort((a, b) => {
if (b.enrolled !== a.enrolled) return b.enrolled - a.enrolled;
if (b.rating !== a.rating) return b.rating - a.rating;
return b.reviewCount - a.reviewCount;
})
.map((entry) => entry.course);
}

/**
* Server component. Fetches courses from /api/courses via the existing
* axios-backed `fetchCourses()` action. Sorts by enrollment then rating and
* surfaces the top items in a client-side embla carousel. Renders nothing
* (graceful degradation) when the API is unreachable or returns no courses.
*/
export default async function FeaturedCourses() {
let courses = [];
try {
courses = await fetchCourses();
} catch (error) {
// eslint-disable-next-line no-console
console.error("[FeaturedCourses] fetch failed:", error?.message || error);
courses = [];
}

const ranked = rankCourses(courses).slice(0, 8);
Comment thread
Whiznificent marked this conversation as resolved.

if (ranked.length === 0) {
// Graceful degradation — silently skip the section.
return null;
}

return (
<section
id="featured-courses"
aria-labelledby="featured-courses-heading"
className="relative mx-auto w-full max-w-7xl px-4 py-16 sm:py-20"
>
<header className="mb-10 flex flex-col items-center text-center sm:flex-row sm:items-end sm:justify-between sm:text-left">
<div className="flex flex-col items-center sm:items-start">
<span className="mb-3 inline-flex items-center gap-1.5 rounded-full border border-green-200/50 bg-gradient-to-r from-green-50/80 to-emerald-50/80 px-3 py-1 text-xs font-medium text-green-700 dark:border-green-500/20 dark:from-green-500/10 dark:to-emerald-500/10 dark:text-green-300">
<Sparkles className="size-3.5" aria-hidden="true" />
Featured Courses
</span>
<h2
id="featured-courses-heading"
className="bg-gradient-to-r from-accent via-green-500 to-highlight bg-clip-text text-3xl font-bold tracking-tight text-transparent sm:text-4xl md:text-5xl"
>
Learn from the Community
</h2>
<p className="mt-2 max-w-xl text-base text-muted-foreground sm:text-lg">
Hand-picked courses from our most engaged educators and learners.
</p>
</div>

<Link
href="/dashboard/courses"
className="mt-4 inline-flex items-center gap-1.5 self-end rounded-full border border-accent/30 px-4 py-2 text-sm font-semibold text-accent transition-colors hover:bg-accent hover:text-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2 sm:mt-0"
>
Browse all courses
<ArrowRight className="size-4" aria-hidden="true" />
</Link>
</header>

<FeaturedCoursesCarousel>
{ranked.map((course) => (
<LandingCourseCard key={course._id || course.id} course={course} />
))}
</FeaturedCoursesCarousel>
</section>
);
}
51 changes: 51 additions & 0 deletions app/(pages)/(landingPage)/FeaturedCoursesCarousel.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"use client";

import * as React from "react";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "@/components/ui/carousel";

/**
* Thin client-side wrapper around the existing embla-based Carousel primitive.
* Server-rendered children are passed through; carousel APIs run only on the client.
*/
export default function FeaturedCoursesCarousel({ children }) {
// Normalise to an array; trust upstream keys so React never falls back to
// an index key (which would mask missing-key bugs in the consumers).
const slides = React.Children.toArray(children);

if (slides.length === 0) return null;

return (
<Carousel
opts={{
align: "start",
skipSnaps: false,
}}
className="w-full px-2 sm:px-4"
>
<CarouselContent className="-ml-2 sm:-ml-4">
{slides.map((child, idx) => (
<CarouselItem
key={child.key ?? `featured-course-${idx}`}
className="pl-2 sm:pl-4 md:basis-1/2 lg:basis-1/3"
>
<div className="h-full pb-2">{child}</div>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious
className="hidden sm:flex -left-2 sm:-left-6"
aria-label="Previous featured course"
/>
<CarouselNext
className="hidden sm:flex -right-2 sm:-right-6"
aria-label="Next featured course"
/>
</Carousel>
);
}
67 changes: 35 additions & 32 deletions app/(pages)/(landingPage)/Hero.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,42 @@ import Button from "@/components/atoms/form/Button";
import Navbar from "@/components/molecules/ladingpage/Navbar";

const Hero = () => {
return (
<main className="relative h-screen flex flex-col bg-basic text-white overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-green-500 via-slate-800 to-green-500 opacity-30 blur-2xl z-0" />
<Navbar />
<div className="relative z-10 flex flex-1 flex-col items-center justify-center space-y-10 text-center sm:font-stretch-125%">
<h1
className={cn(
poppins_600,
"text-6xl lg:text-8xl font-bold mb-4 leading-snug"
)}
>
Welcome to{" "}
<span className="bg-gradient-to-r from-green-400 via-green-500 to-green-600 text-transparent bg-clip-text ">
Deen Bridge
</span>
</h1>
return (
<main className="relative h-screen flex flex-col bg-basic text-white overflow-hidden">
<div
aria-hidden="true"
className="absolute inset-0 bg-gradient-to-br from-green-500 via-slate-800 to-green-500 opacity-30 blur-2xl z-0"
/>
<Navbar />
<div className="relative z-10 flex flex-1 flex-col items-center justify-center space-y-10 text-center sm:font-stretch-125%">
<h1
className={cn(
poppins_600,
"text-6xl lg:text-8xl font-bold mb-4 leading-snug"
)}
>
Welcome to{" "}
<span className="bg-gradient-to-r from-green-400 via-green-500 to-green-600 text-transparent bg-clip-text">
Deen Bridge
</span>
</h1>

<p className="text-lg md:text-xl lg:text-3xl mb-6 text-green-200">
Your journey to{" "}
<span className="text-white font-semibold">excellence</span>
{" "} starts here.
</p>
<Button
wide
round
to="/dashboard"
className=" text-white px-10 py-3 animate-in-out transition-all"
>
Lets Dive
</Button>
</div>
</main>
);
<p className="text-lg md:text-xl lg:text-3xl mb-6 text-green-200">
Your journey to{" "}
<span className="text-white font-semibold">excellence</span> starts
here.
</p>
<Button
wide
round
to="/dashboard"
className="text-white px-10 py-3 animate-in-out transition-all"
>
Lets Dive
</Button>
</div>
</main>
);
};

export default Hero;
89 changes: 89 additions & 0 deletions app/(pages)/(landingPage)/LandingCourseCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import Image from "next/image";
import Link from "next/link";
import { Star, GraduationCap } from "lucide-react";
import { getAverageRating } from "@/hooks/getAverageRating";

/**
* A lightweight, server-renderable course card.
* Used on the public landing page carousel where there is no auth context.
* Avoids useAuth / useBookmark hooks so it can render reliably for visitors.
*/
export default function LandingCourseCard({ course }) {
if (!course) return null;

const {
_id,
title = "Untitled course",
description = "",
thumbnail,
price,
category,
createdBy,
reviews = [],
} = course;
Comment thread
Whiznificent marked this conversation as resolved.

const rating = getAverageRating(reviews);
const reviewCount = Array.isArray(reviews) ? reviews.length : 0;
const priceLabel =
price === 0 || price === undefined || price === null ? "Free" : `$${price}`;
const instructorName = createdBy?.name || "DeenBridge Tutor";

return (
<article className="group relative flex h-full flex-col overflow-hidden rounded-2xl border border-green-200/40 bg-white/90 shadow-md backdrop-blur-xl transition-all hover:-translate-y-1 hover:shadow-xl dark:border-white/10 dark:bg-white/5">
<div className="relative h-48 w-full overflow-hidden">
<Image
src={thumbnail || "/images/dnb.png"}
alt={title}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover transition-transform duration-500 group-hover:scale-105"
loading="lazy"
/>
<div className="absolute inset-0 bg-gradient-to-t from-black/70 via-black/10 to-transparent" />
{category && (
<span className="absolute left-3 top-3 z-10 rounded-full bg-white/85 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-accent shadow">
{category}
</span>
)}
</div>

<div className="flex flex-1 flex-col gap-3 p-5">
<h3 className="line-clamp-1 text-lg font-bold text-accent">{title}</h3>
<p className="line-clamp-2 min-h-[2.5rem] text-sm text-muted-foreground">
{description}
</p>

<div className="mt-auto flex items-center justify-between gap-3 text-sm">
<div className="flex items-center gap-2 text-muted-foreground">
<GraduationCap className="size-4 text-accent" aria-hidden="true" />
<span className="line-clamp-1 font-medium text-foreground/80">
{instructorName}
</span>
</div>
<div className="flex items-center gap-2">
{reviewCount > 0 && (
<span
className="flex items-center gap-1 rounded-full bg-yellow-100 px-2 py-0.5 text-xs font-semibold text-yellow-800 dark:bg-yellow-500/15 dark:text-yellow-300"
aria-label={`Rated ${rating.toFixed(1)} out of 5 from ${reviewCount} reviews`}
>
<Star className="size-3 fill-yellow-500 text-yellow-500" aria-hidden="true" />
{rating.toFixed(1)}
</span>
)}
<span className="rounded-full bg-gradient-to-r from-highlight to-accent px-3 py-1 text-xs font-bold text-white shadow">
{priceLabel}
</span>
</div>
</div>

<Link
href={`/dashboard/courses/${_id}`}
className="mt-1 inline-flex w-full items-center justify-center rounded-full bg-accent px-4 py-2 text-sm font-semibold text-white shadow transition-colors hover:bg-highlight focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent focus-visible:ring-offset-2"
>
Explore Course
<span className="sr-only">: {title}</span>
</Link>
</div>
</article>
);
}
Loading