Skip to content
Merged

Dev #165

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
575f82e
fix: a11y label associations and accessible names (#134)
Times-stack Jul 26, 2026
a388e98
feat(course-progress): add resume playback, completion tracking, and …
BountySpaghetti Jul 26, 2026
73600ba
Merge pull request #147 from BountySpaghetti/feature/course-progress-…
zeemscript Jul 26, 2026
5458763
Revert "Feat/wallet onboarding"
zeemscript Jul 26, 2026
0132bb2
fix: separate accessibleLabel from visible count in ReelActionButton…
Times-stack Jul 26, 2026
c332319
Merge pull request #148 from Deen-Bridge/revert-137-feat/wallet-onboa…
zeemscript Jul 26, 2026
d819a83
fix: include counts and expose pressed state on reaction buttons (Co…
Times-stack Jul 26, 2026
7394082
feat(educators): add public educator storefront pages
BountySpaghetti Jul 26, 2026
7e3fbfd
fix(wallet): handle missing wallet, wrong network, trustline, balance…
BountySpaghetti Jul 26, 2026
6be818a
Merge pull request #146 from Times-stack/fix/a11y-nav-forms-media-134
zeemscript Jul 26, 2026
9caf45f
Merge pull request #149 from BountySpaghetti/feature/public-educator-…
zeemscript Jul 26, 2026
304d8e8
feat(#127): drive video player from real course data
dair7 Jul 26, 2026
1c685a1
feat(dashboard): create global CommandPalette component with keyboard…
emarc99 Jul 27, 2026
75a5760
feat(ui): update CommandDialog primitive to support custom filter opt…
emarc99 Jul 27, 2026
15539b8
feat(navigation): refac Searchbox as command palette trigger and moun…
emarc99 Jul 27, 2026
354a5b7
fix(search): encode query parameters and separate local fallback sear…
emarc99 Jul 27, 2026
f4cb28d
fix(command-palette): ensure focus falls back to search trigger when …
emarc99 Jul 27, 2026
a562951
fix: ignore stale resolutions
emarc99 Jul 27, 2026
f929fa6
fix: only fall backto local fake data on request failure
emarc99 Jul 27, 2026
fe1cc4a
pull updates
emarc99 Jul 27, 2026
fbbd5ea
fix: await dynamic APIs for Next.js 15 compatibility
IamOluwatoyin Jul 27, 2026
0acdfef
Merge pull request #153 from IamOluwatoyin/fix/async-dynamic-apis
zeemscript Jul 27, 2026
abcefe1
Merge branch 'dev' into feat/global-command-palette-universal-search
emarc99 Jul 27, 2026
839d97e
fix: error handling, role reset, and loading UX in TransactionHistory
IamOluwatoyin Jul 27, 2026
68afaf4
Fix: Add error throwing for network request failures
emarc99 Jul 27, 2026
dcd32df
Merge pull request #154 from IamOluwatoyin/fix/transaction-history-er…
zeemscript Jul 28, 2026
56638f7
Merge pull request #152 from emarc99/feat/global-command-palette-univ…
zeemscript Jul 28, 2026
8fe1e55
feat(dashboard): replace placeholder widgets with real dashboard data
IamOluwatoyin Jul 28, 2026
20c6dbe
Merge branch 'dev' into fix/dashboard-real-data
IamOluwatoyin Jul 28, 2026
8fb78f1
Merge pull request #156 from IamOluwatoyin/fix/dashboard-real-data
zeemscript Jul 28, 2026
bf77ccc
fix(modal): reimplement Modal over Radix Dialog for full accessibility
IamOluwatoyin Jul 28, 2026
dd25d84
Merge pull request #157 from IamOluwatoyin/fix/modal-accessibility-radix
zeemscript Jul 28, 2026
a2989bb
chore(deps): remove unused dependencies and dead components
eleven-smg Jul 28, 2026
ea5ef9b
feat(library): add search, filters, sorting, and pagination
eleven-smg Jul 29, 2026
fc806e9
Merge pull request #158 from eleven-smg/chore/remove-unused-deps-and-…
zeemscript Jul 29, 2026
3a1c665
Merge pull request #159 from eleven-smg/feat/library-search-filters-p…
zeemscript Jul 29, 2026
c6e93a7
feat: secure and validate environment configuration
IamOluwatoyin Jul 29, 2026
23a0a99
Merge branch 'dev' into fix/env-configuration-and-validation
IamOluwatoyin Jul 29, 2026
f85d978
fix: make NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME optional in env schema
IamOluwatoyin Jul 29, 2026
b0f0abb
Merge pull request #160 from IamOluwatoyin/fix/env-configuration-and-…
zeemscript Jul 29, 2026
26e358a
Centralize auth state in AuthProvider context and fix cookie inconsis…
BountySpaghetti Jul 29, 2026
15a0f3c
Fix PaymentModal to cancel pending Stellar transactions on close/back
BountySpaghetti Jul 29, 2026
1d1f8f4
feat: migrate all forms to react-hook-form + zod validation
IamOluwatoyin Jul 29, 2026
5c27570
Merge branch 'dev' into feat/rhf-zod-forms
IamOluwatoyin Jul 29, 2026
1ff5b6c
Merge pull request #163 from IamOluwatoyin/feat/rhf-zod-forms
zeemscript Jul 29, 2026
d08eb29
Merge pull request #162 from mayborn005/fix/payment-modal-cleanup
zeemscript Jul 29, 2026
ba7fc00
Merge pull request #161 from mayborn005/fix/auth-context
zeemscript Jul 29, 2026
7431032
Merge pull request #151 from dair7/feature/127-video-player-upgrade
zeemscript Jul 29, 2026
47d74eb
feat(auth): add Sign in with Stellar SEP-10 wallet login
devpassionOX Jul 29, 2026
2c9c6f3
Merge pull request #164 from devpassionOX/fix/sep10-stellar-signin
zeemscript Jul 29, 2026
78c12ed
Merge branch 'dev' into fix/wallet-edge-cases
zeemscript Jul 30, 2026
d75d2aa
Merge pull request #150 from BountySpaghetti/fix/wallet-edge-cases
zeemscript Jul 30, 2026
b783b61
Merge remote-tracking branch 'origin/main' into dev
BountySpaghetti Jul 31, 2026
08a7c45
Merge remote-tracking branch 'origin/dev' into dev
BountySpaghetti Jul 31, 2026
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
40 changes: 29 additions & 11 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
# ─── API ───────────────────────────────────────────
NEXT_PUBLIC_API_URL=http://localhost:5000
# Base URL of the dnb-backend REST API

NEXT_PUBLIC_AI_API_URL=http://localhost:8000
# Base URL of the dnb-ai FastAPI service for the AI assistant

# ─── Stellar ───────────────────────────────────────
NEXT_PUBLIC_STELLAR_NETWORK=testnet
# Stellar network to use: "testnet" or "mainnet"

# ─── Cloudinary ────────────────────────────────────
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
NEXT_PUBLIC_CLOUDINARY_API_KEY=your_cloudinary_api_key
NEXT_PUBLIC_CLOUDINARY_API_SECRET=your_cloudinary_api_secret
NEXT_PUBLIC_CLOUDINARY_URL=your_cloudinary_url
NEXT_PUBLIC_JITSI_DOMAIN=https://meet.jit.si
NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME=your_cloudinary_cloud_name
NEXT_PUBLIC_CLOUDINARY_API_KEY=your_cloudinary_api_key
NEXT_PUBLIC_CLOUDINARY_API_SECRET=your_cloudinary_api_secret
NEXT_PUBLIC_CLOUDINARY_URL=your_cloudinary_url
# Cloudinary cloud name for unsigned uploads (required)

# ─── Jitsi ─────────────────────────────────────────
NEXT_PUBLIC_JITSI_DOMAIN=https://meet.jit.si
# Jitsi Meet domain for video spaces

NEXT_PUBLIC_JITSI_REQUIRE_JWT=false
# Set to "true" if the Jitsi deployment requires a signed JWT token

# ─── Firebase ─────────────────────────────────────
NEXT_PUBLIC_FIREBASE_API_KEY=AIzaSyC8LlmtlWXvbcbyVbdyv4r-tDsGhhukdag
NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN=deen-bridge-22195.firebaseapp.com
NEXT_PUBLIC_FIREBASE_PROJECT_ID=deen-bridge-22195
NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET=deen-bridge-22195.firebasestorage.app
NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID=368531944242
NEXT_PUBLIC_FIREBASE_APP_ID=1:368531944242:web:7994b11820741a69d35d2b
NEXT_PUBLIC_FIREBASE_MEASUREMENT_ID=G-ZZ81THLVCC
# Firebase Web SDK config values (not secrets, but environment-specific)
# NODE_ENV=production
NEXT_PUBLIC_SOCKET_URL=https://dnb-backend-api.onrender.com
DNB_API_URL=https://dnb-backend-api.onrender.com
NEXT_PUBLIC_API_URL=https://dnb-backend-api.onrender.com
NEXT_PUBLIC_STELLAR_NETWORK=testnet
# Default USDC issuer on testnet
NEXT_PUBLIC_USDC_ISSUER=GBBD47IF6LWK7P7MDEVSCWTTCJM4RTQR6TQAEEYFAMSTDPISXN4KPOPI
17 changes: 11 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,12 +86,17 @@ The app runs at `http://localhost:3000`.

### Environment Variables

| Variable | Description |
|----------|-------------|
| `NEXT_PUBLIC_API_URL` | Base URL of the [dnb-backend](https://github.com/Deen-Bridge/dnb-backend) API |
| `NEXT_PUBLIC_STELLAR_NETWORK` | `testnet` or `public` (mainnet) |

See `.env.example` for the full list.
| Variable | Required | Description |
|----------|----------|-------------|
| `NEXT_PUBLIC_API_URL` | No (default `http://localhost:5000`) | Base URL of the [dnb-backend](https://github.com/Deen-Bridge/dnb-backend) API |
| `NEXT_PUBLIC_AI_API_URL` | No (default `http://localhost:8000`) | Base URL of the [dnb-ai](https://github.com/Deen-Bridge/dnb-ai) AI assistant service |
| `NEXT_PUBLIC_STELLAR_NETWORK` | No (default `testnet`) | Stellar network — `testnet` or `mainnet` |
| `NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME` | **Yes** | Cloudinary cloud name for unsigned image/video uploads |
| `NEXT_PUBLIC_JITSI_DOMAIN` | No (default `meet.jit.si`) | Jitsi Meet domain for live video spaces |
| `NEXT_PUBLIC_JITSI_REQUIRE_JWT` | No (default `false`) | Whether the Jitsi deployment requires a signed JWT token |
| `NEXT_PUBLIC_FIREBASE_*` | No | Firebase Web SDK config values (apiKey, authDomain, projectId, etc.) — defaults to the project's current values |

See `.env.example` for the full variable list with example values.

### Scripts

Expand Down
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
Loading
Loading