Skip to content
Open
Show file tree
Hide file tree
Changes from 7 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: 4 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,7 @@ 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

# Sentry error monitoring. Safe to leave unset locally - the app and CI
# build both run fine without it, Sentry is simply disabled.
NEXT_PUBLIC_SENTRY_DSN=
6 changes: 6 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,9 @@ jobs:
env:
NEXT_PUBLIC_API_URL: https://api.example.com
NEXT_PUBLIC_STELLAR_NETWORK: testnet
# Optional: only present on the upstream repo, not on fork PRs.
# When absent, next.config.mjs disables the source-map upload step
# so the build still succeeds.
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
SENTRY_ORG: ${{ secrets.SENTRY_ORG }}
SENTRY_PROJECT: ${{ secrets.SENTRY_PROJECT }}
1 change: 1 addition & 0 deletions .npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
legacy-peer-deps=true
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,22 @@

---

## 📸 Screenshots

| Landing Page | Login / Sign Up |
|:---:|:---:|
| ![Landing Page](docs/screenshots/landing.png) | ![Login](docs/screenshots/login.png) |

| Dashboard | Courses |
|:---:|:---:|
| ![Dashboard](docs/screenshots/dashboard.png) | ![Courses](docs/screenshots/courses.png) |

| Library | Wallet & Payments |
|:---:|:---:|
| ![Library](docs/screenshots/library.png) | ![Wallet](docs/screenshots/wallet.png) |

---

## About

Deen Bridge is a modern learning platform that connects Muslims worldwide with authentic Islamic knowledge. Learners enroll in interactive courses, read from a digital library, join live community spaces, message mentors directly, and get instant answers from an Islamic-knowledge AI assistant. Courses and books are purchased with **USDC on the Stellar network** — non-custodial, with creators paid directly to their own wallets.
Expand Down
48 changes: 47 additions & 1 deletion app/account/settings/page.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"use client";
import React, { useState } from "react";
import React, { useState, useEffect } from "react";
import { useAuth } from "@/hooks/useAuth";
import {
Card,
Expand Down Expand Up @@ -46,6 +46,29 @@ const SettingsPage = () => {
const [showNewPassword, setShowNewPassword] = useState(false);
const [message, setMessage] = useState({ type: "", text: "" });

const [deferredPrompt, setDeferredPrompt] = useState(null);
const [installable, setInstallable] = useState(false);

useEffect(() => {
const handler = (e) => {
e.preventDefault();
setDeferredPrompt(e);
setInstallable(true);
};
window.addEventListener("beforeinstallprompt", handler);
return () => window.removeEventListener("beforeinstallprompt", handler);
}, []);

const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const result = await deferredPrompt.userChoice;
if (result.outcome === "accepted") {
setInstallable(false);
}
setDeferredPrompt(null);
};
Comment on lines +62 to +70

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Install banner keeps showing but silently stops working after a dismissed prompt.

installable is only reset when outcome === "accepted". If the user dismisses the native prompt, deferredPrompt is nulled but installable stays true, so a subsequent click on "Install" silently no-ops (the !deferredPrompt guard returns early). The banner should also hide (or re-arm) after a dismissal.

♻️ Proposed fix
   const handleInstall = async () => {
     if (!deferredPrompt) return;
     deferredPrompt.prompt();
     const result = await deferredPrompt.userChoice;
-    if (result.outcome === "accepted") {
-      setInstallable(false);
-    }
+    setInstallable(false);
     setDeferredPrompt(null);
   };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const result = await deferredPrompt.userChoice;
if (result.outcome === "accepted") {
setInstallable(false);
}
setDeferredPrompt(null);
};
const handleInstall = async () => {
if (!deferredPrompt) return;
deferredPrompt.prompt();
const result = await deferredPrompt.userChoice;
setInstallable(false);
setDeferredPrompt(null);
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/account/settings/page.jsx` around lines 62 - 70, Update handleInstall so
installable is reset when the native prompt is dismissed as well as when it is
accepted. Preserve clearing deferredPrompt after userChoice, ensuring the banner
cannot remain visible while the prompt reference is unavailable.


// Profile state
const [profile, setProfile] = useState({
avatar: user?.avatar || "",
Expand Down Expand Up @@ -213,6 +236,29 @@ const SettingsPage = () => {
</AlertDescription>
</Alert>
)}

{installable && (
<div className="mb-4 flex items-center justify-between rounded-xl border border-accent/20 bg-accent/5 p-4">
<div className="flex items-center gap-3">
<Smartphone className="h-5 w-5 text-accent" />
<p className="text-sm font-medium text-foreground">
Install DeenBridge for quick access
</p>
</div>
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => setInstallable(false)}
>
Dismiss
</Button>
<Button size="sm" onClick={handleInstall}>
Install
</Button>
</div>
</div>
)}
</div>
<Tabs
value={activeTab}
Expand Down
36 changes: 36 additions & 0 deletions app/dashboard/courses/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";

export default function CoursesError({ error, reset }) {
const [eventId, setEventId] = useState(null);

useEffect(() => {
const id = Sentry.captureException(error);
setEventId(id);
}, [error]);

return (
<div className="flex-1 w-full flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="max-w-md">
<h2 className="text-xl font-bold text-[#252F40] mb-3">
Couldn&apos;t load this course
</h2>
<p className="text-sm text-gray-500 mb-8">
We ran into a problem fetching your course content. Please retry.
</p>
<button
type="button"
onClick={() => reset()}
className="px-5 py-2.5 rounded-lg bg-[#34AD5D] text-white text-sm font-medium hover:bg-[#2c9350] transition-colors"
>
Retry
</button>
{eventId && (
<p className="mt-6 text-xs text-gray-400">Report ID: {eventId}</p>
)}
</div>
</div>
);
}
39 changes: 39 additions & 0 deletions app/dashboard/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
"use client";

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";

// Renders inside app/dashboard/layout.jsx's <SidebarInset>, so the sidebar
// and nav header stay usable while just this segment shows the fallback.
export default function DashboardError({ error, reset }) {
const [eventId, setEventId] = useState(null);

useEffect(() => {
const id = Sentry.captureException(error);
setEventId(id);
}, [error]);

return (
<div className="flex-1 w-full flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="max-w-md">
<h2 className="text-xl font-bold text-[#252F40] mb-3">
This page ran into a problem
</h2>
<p className="text-sm text-gray-500 mb-8">
Something went wrong loading your dashboard. Try again, or use the
sidebar to head somewhere else.
</p>
<button
type="button"
onClick={() => reset()}
className="px-5 py-2.5 rounded-lg bg-[#34AD5D] text-white text-sm font-medium hover:bg-[#2c9350] transition-colors"
>
Try again
</button>
{eventId && (
<p className="mt-6 text-xs text-gray-400">Report ID: {eventId}</p>
)}
</div>
</div>
);
}
36 changes: 36 additions & 0 deletions app/dashboard/library/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"use client";

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";

export default function LibraryError({ error, reset }) {
const [eventId, setEventId] = useState(null);

useEffect(() => {
const id = Sentry.captureException(error);
setEventId(id);
}, [error]);

return (
<div className="flex-1 w-full flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="max-w-md">
<h2 className="text-xl font-bold text-[#252F40] mb-3">
Couldn&apos;t load your library
</h2>
<p className="text-sm text-gray-500 mb-8">
We ran into a problem fetching this content. Please retry.
</p>
<button
type="button"
onClick={() => reset()}
className="px-5 py-2.5 rounded-lg bg-[#34AD5D] text-white text-sm font-medium hover:bg-[#2c9350] transition-colors"
>
Retry
</button>
{eventId && (
<p className="mt-6 text-xs text-gray-400">Report ID: {eventId}</p>
)}
</div>
</div>
);
}
48 changes: 48 additions & 0 deletions app/error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"use client";

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";

// Kept dependency-light on purpose (no toasts, providers, or design-system
// imports) so this boundary can't itself throw while trying to render a
// recovery UI for an unrelated error.
export default function Error({ error, reset }) {
const [eventId, setEventId] = useState(null);

useEffect(() => {
const id = Sentry.captureException(error);
setEventId(id);
}, [error]);

return (
<div className="min-h-[60vh] w-full flex flex-col items-center justify-center px-6 py-16 text-center">
<div className="max-w-md">
<h1 className="text-2xl font-bold text-[#252F40] mb-3">
Something went wrong
</h1>
<p className="text-sm text-gray-500 mb-8">
An unexpected error occurred while loading this page. You can try
again, or head back to the homepage.
</p>
<div className="flex items-center justify-center gap-3">
<button
type="button"
onClick={() => reset()}
className="px-5 py-2.5 rounded-lg bg-[#34AD5D] text-white text-sm font-medium hover:bg-[#2c9350] transition-colors"
>
Try again
</button>
<a
href="/"
className="px-5 py-2.5 rounded-lg border border-gray-200 text-[#252F40] text-sm font-medium hover:bg-gray-50 transition-colors"
>
Go home
</a>
</div>
{eventId && (
<p className="mt-6 text-xs text-gray-400">Report ID: {eventId}</p>
)}
</div>
</div>
);
}
69 changes: 69 additions & 0 deletions app/global-error.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"use client";

import { useEffect, useState } from "react";
import * as Sentry from "@sentry/nextjs";

// This only activates in production builds (test with `npm run build && npm
// start`). No app providers, fonts, or global stylesheet are guaranteed to
// be available here, so this stays fully self-contained with inline styles
// and renders its own <html>/<body>.
export default function GlobalError({ error, reset }) {
const [eventId, setEventId] = useState(null);

useEffect(() => {
const id = Sentry.captureException(error);
setEventId(id);
}, [error]);

return (
<html lang="en">
<body
style={{
margin: 0,
minHeight: "100vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "24px",
textAlign: "center",
fontFamily:
"-apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica, Arial, sans-serif",
backgroundColor: "#ffffff",
color: "#252F40",
}}
>
<div style={{ maxWidth: 420 }}>
<h1 style={{ fontSize: 24, fontWeight: 700, marginBottom: 12 }}>
Deen Bridge
</h1>
<p style={{ fontSize: 15, color: "#6b7280", marginBottom: 28 }}>
Something went wrong and this page couldn&apos;t load. Please
reload to try again.
</p>
<button
type="button"
onClick={() => reset()}
style={{
padding: "10px 20px",
borderRadius: 8,
border: "none",
backgroundColor: "#34AD5D",
color: "#ffffff",
fontSize: 14,
fontWeight: 500,
cursor: "pointer",
}}
>
Reload
</button>
{eventId && (
<p style={{ marginTop: 20, fontSize: 12, color: "#9ca3af" }}>
Report ID: {eventId}
</p>
)}
</div>
</body>
</html>
);
}
16 changes: 16 additions & 0 deletions app/layout.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,22 @@ export const metadata = {
title: "Deen Bridge",
description:
"Empowering Muslims with authentic knowledge — Learn Qur'an, Arabic, Fiqh, and more through 1-on-1 live mentorship and lots more.",
manifest: "/manifest",
icons: {
Comment on lines +21 to +22

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== files ==\n'
git ls-files 'app/layout.js' 'app/**/manifest.*' 'public/sw.js'

printf '\n== outline: app/layout.js ==\n'
ast-grep outline app/layout.js --view expanded || true

printf '\n== relevant layout lines ==\n'
sed -n '1,120p' app/layout.js

printf '\n== manifest files ==\n'
for f in $(git ls-files 'app/**/manifest.*'); do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,220p' "$f"
done

printf '\n== sw.js manifest references ==\n'
rg -n 'manifest|webmanifest' public/sw.js app -g '!**/node_modules/**' || true

Repository: Deen-Bridge/dnb-frontend

Length of output: 33106


Fix the manifest URL
app/manifest.js is served as /manifest.webmanifest, so manifest: "/manifest" points at the wrong path and breaks the manifest link for installability.

🐛 Proposed fix
-  manifest: "/manifest",
+  manifest: "/manifest.webmanifest",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
manifest: "/manifest",
icons: {
manifest: "/manifest.webmanifest",
icons: {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/layout.js` around lines 21 - 22, Update the manifest value in the layout
metadata to reference the actual `/manifest.webmanifest` endpoint instead of
`/manifest`, while leaving the surrounding icons configuration unchanged.

icon: [
{ url: "/favicon.ico", sizes: "any" },
{ url: "/icons/icon-192x192.png", sizes: "192x192", type: "image/png" },
{ url: "/icons/icon-512x512.png", sizes: "512x512", type: "image/png" },
],
apple: [
{ url: "/icons/icon-192x192.png", sizes: "192x192" },
],
},
appleWebApp: {
capable: true,
title: "DeenBridge",
statusBarStyle: "black-translucent",
},
openGraph: {
title: "Deen Bridge ",
description:
Expand Down
Loading