diff --git a/app/(pages)/educators/[profileid]/EducatorPageClient.jsx b/app/(pages)/educators/[profileid]/EducatorPageClient.jsx
new file mode 100644
index 0000000..f4d92a6
--- /dev/null
+++ b/app/(pages)/educators/[profileid]/EducatorPageClient.jsx
@@ -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 ;
+
+ if (error || !educator) {
+ return (
+
+
+
+
+
+
+
+ );
+ }
+
+ const hasContent =
+ courses.length > 0 || books.length > 0 || spaces.length > 0;
+
+ if (!hasContent) {
+ return (
+
+
+
+
+
+
+
No public content yet
+
+ This educator hasn't published any courses, books, or spaces yet.
+
+
+
+
+
+ );
+ }
+
+ 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 (
+
+
+
+
+
+ {tabs.length > 1 && (
+
+
+ {tabs.map((tab) => (
+
+ ))}
+
+
+ )}
+
+
+ {activeTab === "courses" && courses.length > 0 && (
+
+ {courses.map((course) => (
+
+ ))}
+
+ )}
+ {activeTab === "books" && books.length > 0 && (
+
+ {books.map((book) => (
+
+ ))}
+
+ )}
+ {activeTab === "spaces" && spaces.length > 0 && (
+
+ {spaces.map((space) => (
+
+ ))}
+
+ )}
+
+
+
+
+ );
+}
diff --git a/app/(pages)/educators/[profileid]/page.jsx b/app/(pages)/educators/[profileid]/page.jsx
new file mode 100644
index 0000000..bb96482
--- /dev/null
+++ b/app/(pages)/educators/[profileid]/page.jsx
@@ -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 ;
+}
diff --git a/app/account/profile/[profileid]/page.jsx b/app/account/profile/[profileid]/page.jsx
index 773b930..61edd75 100644
--- a/app/account/profile/[profileid]/page.jsx
+++ b/app/account/profile/[profileid]/page.jsx
@@ -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";
@@ -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");
@@ -56,6 +58,13 @@ const page = ({ params }) => {
diff --git a/components/molecules/dashboard/cards/courseCard.jsx b/components/molecules/dashboard/cards/courseCard.jsx
index 8886f9e..48120c6 100644
--- a/components/molecules/dashboard/cards/courseCard.jsx
+++ b/components/molecules/dashboard/cards/courseCard.jsx
@@ -62,7 +62,7 @@ const CourseCard = ({ course, onBookmarkChange, initialIsBookmarked }) => {
diff --git a/components/molecules/dashboard/cards/educators/PublicBookCard.jsx b/components/molecules/dashboard/cards/educators/PublicBookCard.jsx
new file mode 100644
index 0000000..0df1546
--- /dev/null
+++ b/components/molecules/dashboard/cards/educators/PublicBookCard.jsx
@@ -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 (
+
+
+
+
+
{book.title}
+
+
+ {book.category || "General"}
+
+
+ {book.price > 0 ? `$${book.price}` : "Free"}
+
+
+
+
+
+
+
+
{book.readCount || 0} readers
+ {avgRating > 0 && (
+
+ {[...Array(5)].map((_, i) => (
+
+ ))}
+
+ )}
+
+
+
+
+
+
+
+ );
+};
+
+export default PublicBookCard;
diff --git a/components/molecules/dashboard/cards/educators/PublicCourseCard.jsx b/components/molecules/dashboard/cards/educators/PublicCourseCard.jsx
new file mode 100644
index 0000000..c107638
--- /dev/null
+++ b/components/molecules/dashboard/cards/educators/PublicCourseCard.jsx
@@ -0,0 +1,71 @@
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import Button from "@/components/atoms/form/Button";
+import Link from "next/link";
+import Image from "next/image";
+import { Star } from "lucide-react";
+import { getAverageRating } from "@/hooks/getAverageRating";
+
+const PublicCourseCard = ({ course }) => {
+ const avgRating = getAverageRating(course?.reviews);
+
+ return (
+
+
+
+
+
+
+ {course.category || "General"}
+
+
+
+
+
+
+ {course.title}
+
+
+ {course.description}
+
+
+
+
+
+
+ {avgRating > 0 && (
+
+
+
+ {avgRating.toFixed(1)}
+
+
+ )}
+
+
+ {course.price ? `$${course.price}` : "Free"}
+
+
+
+
+
+
+
+
+ );
+};
+
+export default PublicCourseCard;
diff --git a/components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx b/components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx
new file mode 100644
index 0000000..68de9b9
--- /dev/null
+++ b/components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx
@@ -0,0 +1,97 @@
+"use client";
+
+import { Card, CardTitle } from "@/components/ui/card";
+import { Badge } from "@/components/ui/badge";
+import Button from "@/components/atoms/form/Button";
+import Image from "next/image";
+import { VideoIcon, Clock } from "lucide-react";
+import { format } from "date-fns";
+
+const PublicSpaceCard = ({ space }) => {
+ const {
+ _id,
+ title,
+ description,
+ thumbnail,
+ category,
+ status,
+ eventDate,
+ duration,
+ } = space;
+
+ function formatDuration(minutes) {
+ if (minutes < 60) return `${minutes} mins`;
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+ return mins === 0
+ ? `${hours} hr${hours > 1 ? "s" : ""}`
+ : `${hours} hr${hours > 1 ? "s" : ""} ${mins} min${mins > 1 ? "s" : ""}`;
+ }
+
+ const formattedTime = eventDate
+ ? format(new Date(eventDate), "PPpp")
+ : "TBD";
+
+ return (
+
+
+
+
+
+ {category && (
+
+ {category}
+
+ )}
+ {status && (
+
+ {status.toUpperCase()}
+
+ )}
+
+
+
+
+
+
+ {title}
+
+
+ {description}
+
+
+
+
+
+
+ {formattedTime}
+
+ {duration && (
+
+ {formatDuration(duration)}
+
+ )}
+
+
+
+
+
+ );
+};
+
+export default PublicSpaceCard;
diff --git a/components/molecules/dashboard/cards/libraryCard.jsx b/components/molecules/dashboard/cards/libraryCard.jsx
index 61ae03c..da7505f 100644
--- a/components/molecules/dashboard/cards/libraryCard.jsx
+++ b/components/molecules/dashboard/cards/libraryCard.jsx
@@ -47,7 +47,7 @@ const LibraryBookCard = ({ book, onBookmarkChange, initialIsBookmarked }) => {
{/* Author */}
diff --git a/components/molecules/dashboard/cards/spaceCard.jsx b/components/molecules/dashboard/cards/spaceCard.jsx
index 6cbcb83..c206ce1 100644
--- a/components/molecules/dashboard/cards/spaceCard.jsx
+++ b/components/molecules/dashboard/cards/spaceCard.jsx
@@ -6,6 +6,7 @@ import {
} from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import Button from "@/components/atoms/form/Button";
+import Link from "next/link";
import Image from "next/image";
import {
Avatar,
@@ -90,7 +91,10 @@ const SpaceCard = ({ space }) => {
{/* Host */}
-
+
{
{host?.name || "Ustadh Ahmad"}
Host
-
+
diff --git a/components/organisms/educators/EducatorProfileHeader.jsx b/components/organisms/educators/EducatorProfileHeader.jsx
new file mode 100644
index 0000000..63dacc6
--- /dev/null
+++ b/components/organisms/educators/EducatorProfileHeader.jsx
@@ -0,0 +1,146 @@
+"use client";
+
+import React from "react";
+import Link from "next/link";
+import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
+import Button from "@/components/atoms/form/Button";
+import { cn } from "@/lib/utils";
+import {
+ Copy,
+ Share2,
+ Users,
+ BookOpen,
+ GraduationCap,
+ Star,
+ Calendar,
+ ArrowLeft,
+} from "lucide-react";
+import { format } from "date-fns";
+
+const EducatorProfileHeader = ({
+ educator,
+ followersCount,
+ avgRating,
+ isFollowing,
+ followLoading,
+ onFollowToggle,
+ onShare,
+ isOwnProfile,
+ stats,
+}) => {
+ return (
+
+
+
+
+
+
+
+
+ {educator?.name?.charAt(0) || "E"}
+
+
+
+
+
+
+
+
+ {educator?.name}
+
+ {educator?.role && (
+
{educator.role}
+ )}
+ {educator?.bio && (
+
+ {educator.bio}
+
+ )}
+ {educator?.createdAt && (
+
+
+ Joined{" "}
+ {format(new Date(educator.createdAt), "MMMM yyyy")}
+
+ )}
+
+
+
+
+ {isOwnProfile ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+
+ {followersCount}
+ Followers
+
+ {stats && stats.courses > 0 && (
+
+
+ {stats.courses}
+ Courses
+
+ )}
+ {stats && stats.books > 0 && (
+
+
+ {stats.books}
+ Books
+
+ )}
+ {stats && stats.spaces > 0 && (
+
+
+ {stats.spaces}
+ Spaces
+
+ )}
+ {avgRating > 0 && (
+
+
+
+ {avgRating.toFixed(1)}
+
+ Rating
+
+ )}
+
+
+
+
+ );
+};
+
+export default EducatorProfileHeader;