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
271 changes: 271 additions & 0 deletions app/(pages)/educators/[profileid]/EducatorPageClient.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
"use client";
import React, { use, useState, useEffect } from "react";
import Link from "next/link";
import { useAuth } from "@/hooks/useAuth";
import { useRouter } from "next/navigation";
import { getUserById } from "@/lib/actions/users/getUserById";
import { fetchUserCourses } from "@/lib/actions/courses/fetch-user-id-courses";
import { fetchUserBooks } from "@/lib/actions/library/fetch-user-id-books";
import { fetchUserSpaces } from "@/lib/actions/spaces/fetchUserSpaces";
import {
getFollowersCount,
checkIfFollowing,
followUser,
unfollowUser,
} from "@/hooks/useFollow";
import { getAverageRating } from "@/hooks/getAverageRating";
import EducatorProfileHeader from "@/components/organisms/educators/EducatorProfileHeader";
import PublicCourseCard from "@/components/molecules/dashboard/cards/educators/PublicCourseCard";
import PublicBookCard from "@/components/molecules/dashboard/cards/educators/PublicBookCard";
import PublicSpaceCard from "@/components/molecules/dashboard/cards/educators/PublicSpaceCard";
import NotFoundComp from "@/components/molecules/errors/NotFound";
import NetworkErrorComp from "@/components/molecules/errors/NetworkError";
import Loader from "@/components/molecules/loaders/rootLoader";
import Footer from "@/components/molecules/ladingpage/Footer";
import Navbar from "@/components/molecules/ladingpage/Navbar";
import { Copy, Share2, Users, BookOpen, GraduationCap, Star } from "lucide-react";
import { toast } from "sonner";
import { cn } from "@/lib/utils";
import Button from "@/components/atoms/form/Button";

export default function PublicEducatorPage({ params }) {
const { profileid } = use(params);
const { user: currentUser } = useAuth();
const router = useRouter();

const [educator, setEducator] = useState(null);
const [courses, setCourses] = useState([]);
const [books, setBooks] = useState([]);
const [spaces, setSpaces] = useState([]);
const [followersCount, setFollowersCount] = useState(0);
const [isFollowing, setIsFollowing] = useState(false);
const [followLoading, setFollowLoading] = useState(false);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(false);
const [activeTab, setActiveTab] = useState("courses");

useEffect(() => {
async function load() {
setLoading(true);
setError(false);
try {
const res = await getUserById(profileid);
const u = res?.user || null;
if (!u) {
setError(true);
setLoading(false);
return;
}
setEducator(u);

const [coursesData, booksData, spacesData, followersRes] =
await Promise.allSettled([
fetchUserCourses(profileid),
fetchUserBooks(profileid),
fetchUserSpaces(profileid),
getFollowersCount(profileid),
]);

if (coursesData.status === "fulfilled") {
setCourses(Array.isArray(coursesData.value) ? coursesData.value : []);
}
if (booksData.status === "fulfilled") {
setBooks(Array.isArray(booksData.value) ? booksData.value : []);
}
if (spacesData.status === "fulfilled") {
const sd = spacesData.value;
setSpaces(sd?.spaces || (Array.isArray(sd) ? sd : []));
}
if (followersRes.status === "fulfilled" && followersRes.value?.success) {
setFollowersCount(
followersRes.value.followersCount || followersRes.value.count || 0
);
}

if (currentUser?._id && currentUser._id !== profileid) {
const followRes = await checkIfFollowing(profileid);
if (followRes?.success) {
setIsFollowing(followRes.isFollowing);
}
}
} catch (e) {
setError(true);
} finally {
setLoading(false);
}
}
load();
}, [profileid, currentUser?._id]);

const handleFollowToggle = async () => {
if (!currentUser?._id) {
router.push(`/login?next=${encodeURIComponent(`/educators/${profileid}`)}`);
return;
}
setFollowLoading(true);
try {
const result = isFollowing
? await unfollowUser(profileid)
: await followUser(profileid);
if (result.success) {
setIsFollowing(!isFollowing);
setFollowersCount((c) => (isFollowing ? c - 1 : c + 1));
} else {
toast.error(result.message || "Failed to update follow status");
}
} catch {
toast.error("Failed to update follow status");
} finally {
setFollowLoading(false);
}
};

const handleShare = async () => {
const url = `${typeof window !== "undefined" ? window.location.origin : ""}/educators/${profileid}`;
if (navigator.share) {
try {
await navigator.share({
title: educator?.name || "Educator Profile",
text: educator?.bio || `Check out ${educator?.name} on DeenBridge`,
url,
});
} catch {}
} else {
try {
await navigator.clipboard.writeText(url);
toast.success("Link copied to clipboard!");
} catch {
toast.error("Failed to copy link");
}
}
};

if (loading) return <Loader />;

if (error || !educator) {
return (
<div className="min-h-screen bg-basic flex flex-col">
<Navbar />
<main className="flex-1 flex items-center justify-center">
<NotFoundComp errMsg="Educator profile not found or is private." />
</main>
<Footer />
</div>
);
}

const hasContent =
courses.length > 0 || books.length > 0 || spaces.length > 0;

if (!hasContent) {
return (
<div className="min-h-screen bg-basic flex flex-col">
<Navbar />
<main className="flex-1">
<EducatorProfileHeader
educator={educator}
followersCount={followersCount}
isFollowing={isFollowing}
followLoading={followLoading}
onFollowToggle={handleFollowToggle}
onShare={handleShare}
isOwnProfile={currentUser?._id === profileid}
/>
<div className="max-w-4xl mx-auto px-4 py-16 text-center">
<BookOpen className="h-12 w-12 text-muted-foreground mx-auto mb-4" />
<h3 className="text-lg font-semibold mb-2">No public content yet</h3>
<p className="text-muted-foreground">
This educator hasn&apos;t published any courses, books, or spaces yet.
</p>
</div>
</main>
<Footer />
</div>
);
}

const tabs = [
{ key: "courses", label: "Courses", count: courses.length, icon: GraduationCap },
{ key: "books", label: "Books", count: books.length, icon: BookOpen },
{ key: "spaces", label: "Spaces", count: spaces.length, icon: Users },
].filter((t) => t.count > 0);

const allRatings = [
...courses.flatMap((c) => c.reviews || []),
...books.flatMap((b) => b.reviews || []),
];
const avgRating = getAverageRating(allRatings);

return (
<div className="min-h-screen bg-basic flex flex-col">
<Navbar />
<main className="flex-1">
<EducatorProfileHeader
educator={educator}
followersCount={followersCount}
avgRating={avgRating}
isFollowing={isFollowing}
followLoading={followLoading}
onFollowToggle={handleFollowToggle}
onShare={handleShare}
isOwnProfile={currentUser?._id === profileid}
stats={{
courses: courses.length,
books: books.length,
spaces: spaces.length,
}}
/>

{tabs.length > 1 && (
<div className="max-w-6xl mx-auto px-4 sm:px-6 mt-6">
<div className="flex gap-2 border-b border-border pb-0">
{tabs.map((tab) => (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={cn(
"flex items-center gap-2 px-4 py-3 text-sm font-medium border-b-2 transition-colors",
activeTab === tab.key
? "border-accent text-accent"
: "border-transparent text-muted-foreground hover:text-foreground"
)}
>
<tab.icon className="h-4 w-4" />
{tab.label}
<span className="text-xs bg-muted px-1.5 py-0.5 rounded-full">
{tab.count}
</span>
</button>
))}
</div>
</div>
)}

<div className="max-w-6xl mx-auto px-4 sm:px-6 py-8">
{activeTab === "courses" && courses.length > 0 && (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{courses.map((course) => (
<PublicCourseCard key={course._id} course={course} />
))}
</div>
)}
{activeTab === "books" && books.length > 0 && (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{books.map((book) => (
<PublicBookCard key={book._id} book={book} />
))}
</div>
)}
{activeTab === "spaces" && spaces.length > 0 && (
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">
{spaces.map((space) => (
<PublicSpaceCard key={space._id} space={space} />
))}
</div>
)}
</div>
</main>
<Footer />
</div>
);
}
24 changes: 24 additions & 0 deletions app/(pages)/educators/[profileid]/page.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import EducatorPageClient from "./EducatorPageClient";

export async function generateMetadata({ params }) {
const { profileid } = await params;
return {
title: "Educator Profile - Deen Bridge",
description: "View this educator's courses, books, and spaces on Deen Bridge.",
openGraph: {
title: "Educator Profile - Deen Bridge",
description: "View this educator's courses, books, and spaces on Deen Bridge.",
url: `https://deenbridge.com/educators/${profileid}`,
type: "profile",
},
twitter: {
card: "summary_large_image",
title: "Educator Profile - Deen Bridge",
description: "View this educator's courses, books, and spaces on Deen Bridge.",
},
};
}

export default function EducatorPage({ params }) {
return <EducatorPageClient params={params} />;
}
9 changes: 9 additions & 0 deletions app/account/profile/[profileid]/page.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";
import React, { use, useState, useEffect } from "react";
import Link from "next/link";
import ProfileHeader from "@/components/organisms/account/profile/ProfileHeader";
import ProfileUserInfo from "@/components/organisms/account/profile/ProfileUserInfo";
import ProfileTabs from "@/components/organisms/account/profile/ProfileTabs";
Expand All @@ -8,6 +9,7 @@ import { getUserById } from "@/lib/actions/users/getUserById";
import NotFoundComp from "@/components/molecules/errors/NotFound";
import NetworkErrorComp from "@/components/molecules/errors/NetworkError";
import Loader from "@/components/molecules/loaders/rootLoader";
import { ExternalLink } from "lucide-react";
const page = ({ params }) => {
const { profileid } = use(params);
const [selectedTab, setSelectedTab] = useState("courses");
Expand Down Expand Up @@ -56,6 +58,13 @@ const page = ({ params }) => {
<div className="min-h-screen bg-muted p-2 sm:p-4">
<ProfileHeader avatar={user?.avatar} />
<ProfileUserInfo user={user} />
<Link
href={`/educators/${profileid}`}
className="inline-flex items-center gap-1.5 text-sm text-accent hover:underline mb-4 mx-2 sm:mx-4"
>
<ExternalLink className="h-3.5 w-3.5" />
View public page
</Link>
<ProfileTabs selectedTab={selectedTab} onChange={setSelectedTab} />
<ProfileContent selectedTab={selectedTab} profileId={user?._id} />
</div>
Expand Down
2 changes: 1 addition & 1 deletion components/molecules/dashboard/cards/courseCard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ const CourseCard = ({ course, onBookmarkChange, initialIsBookmarked }) => {
<CardContent>
<div className="flex justify-between items-center gap-3">
<Link
href={`/account/profile/${course.createdBy?._id}`}
href={`/educators/${course.createdBy?._id}`}
className="flex items-center gap-2"
>
<Avatar className="h-10 w-10 rounded-lg">
Expand Down
63 changes: 63 additions & 0 deletions components/molecules/dashboard/cards/educators/PublicBookCard.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import Image from "next/image";
import Button from "@/components/atoms/form/Button";
import { Star } from "lucide-react";
import { getAverageRating } from "@/hooks/getAverageRating";

const PublicBookCard = ({ book }) => {
const avgRating = getAverageRating(book?.reviews);

return (
<div className="bg-white rounded-2xl overflow-hidden shadow-md w-full max-w-md mx-auto">
<div className="relative w-full h-64">
<Image
src={book.image || "/images/placeholder.jpg"}
alt={book.title}
fill
className="object-cover"
/>
<div className="absolute bottom-0 left-0 w-full bg-gradient-to-t from-black/80 to-transparent text-white px-4 py-3 space-y-1">
<h4 className="text-sm font-semibold truncate">{book.title}</h4>
<div className="flex justify-between items-center text-xs">
<span className="bg-white/20 px-2 py-0.5 rounded">
{book.category || "General"}
</span>
<span className="font-medium">
{book.price > 0 ? `$${book.price}` : "Free"}
</span>
</div>
</div>
</div>

<div className="px-4 py-4 flex flex-col gap-3 text-sm text-muted-foreground">
<div className="flex items-center justify-between text-xs">
<span>{book.readCount || 0} readers</span>
{avgRating > 0 && (
<div className="flex items-center gap-0.5 text-yellow-500">
{[...Array(5)].map((_, i) => (
<Star
key={`${book._id}-star-${i}`}
size={14}
fill={i < Math.round(avgRating) ? "#FFD700" : "none"}
stroke="#FFD700"
/>
))}
</div>
)}
</div>
</div>

<div className="px-4 py-3">
<Button
to={`/dashboard/library/${book._id}`}
wide
round
className="w-full bg-accent text-white"
>
View Book
</Button>
</div>
</div>
);
};

export default PublicBookCard;
Loading
Loading