Skip to content

Merge pull request #138 from Ahbiz/feature/issue-132 unified saved hub and bookmark refactor (Closes #132) - #141

Merged
zeemscript merged 18 commits into
mainfrom
dev
Jul 24, 2026
Merged

Merge pull request #138 from Ahbiz/feature/issue-132 unified saved hub and bookmark refactor (Closes #132)#141
zeemscript merged 18 commits into
mainfrom
dev

Conversation

@zeemscript

@zeemscript zeemscript commented Jul 24, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features
    • Added earnings dashboard with revenue charts, top items, balances, and transaction statuses.
    • Added views for purchased and saved courses and books, including receipts and bookmarking.
    • Added prayer times with location selection, countdowns, and Hijri dates.
    • Added progressive web app installation support and offline access.
    • Added dashboard navigation for Saved, My Purchases, and Earnings.
  • Documentation
    • Expanded README screenshots to cover key app pages.
  • Improvements
    • Enhanced caching for faster loading and improved offline behavior.
    • Added clearer loading, error, and empty states throughout new dashboard views.

abimbolaalabi and others added 18 commits July 21, 2026 08:49
feat(pwa): add offline support and installable PWA with Serwist
docs(readme): add live application screenshots
- Create `useEarnings` hook to aggregate creator transaction history into earnings analytics
- Fetch all paginated creator-role transactions and separate confirmed, pending, submitted, failed, and expired states
- Calculate total confirmed earnings, sales count, month-over-month comparison, and withdrawable USDC balance
- Aggregate confirmed revenue into time-series data for analytics charts with 7-day, 30-day, and all-time range filters
- Build per-item revenue and units-sold breakdown with links to related courses and books
- Fetch creator courses and books to resolve item navigation within the dashboard

- Add `/dashboard/earnings` page with:
  - Summary cards for total earnings, sales count, monthly trend, and withdrawable balance
  - Revenue-over-time chart using the shared chart components
  - Top-selling courses and books ranked by revenue
  - Transaction status breakdown with color-coded badges
  - Loading skeletons, empty state, error state with retry, and no-wallet state
  - Clear indication that only confirmed transactions contribute to earnings totals

- Add an "Earnings" navigation item to the dashboard sidebar for quick access

Implementation notes:
- Reuses the existing Stellar transaction history and wallet infrastructure
- Ensures earnings calculations include only confirmed transactions while exposing unsettled transaction statuses separately
- Supports pagination-safe aggregation by fetching all creator transaction pages
- Integrates with the existing chart wrapper and navigation structure without duplicating wallet transaction logic

Verification:
- Confirmed summary metrics, charts, and per-item analytics render correctly
- Verified loading, empty, error, and no-wallet states
- `npm run lint` passes successfully
- `npm run build` introduces no new build issues (existing Firebase/Firestore build errors are unrelated to this implementation)
…d-enchancement

ο»Ώfeat(dashboard): add educator earnings analytics dashboard
- Create `usePurchases` hook to aggregate owned courses and books with corresponding Stellar payment transactions
- Correlate purchased content using buyer-role transaction history while supporting pagination-safe aggregation
- Handle free enrollments gracefully by distinguishing unpaid content from paid purchases
- Expose reusable receipt lookup logic for retrieving confirmed transaction details per purchased item

- Add `/dashboard/purchases` page featuring:
  - Separate Courses and Books tabs with owned item counts
  - Responsive purchase cards with thumbnails, category badges, ownership status, pricing, and direct Watch/Read actions
  - Purchase receipt modal displaying:
    - USDC payment amount
    - Transaction status
    - Purchase date
    - Creator information
    - Truncated wallet addresses
    - Stellar Explorer transaction link
  - Printable browser-friendly receipt view
  - "Free Enrollment" state for items without an associated on-chain transaction
  - Loading skeletons, empty state with browse CTAs, and error state with retry support

- Add "My Purchases" navigation item to the dashboard sidebar with ShoppingBag icon

Implementation notes:
- Reuses existing purchase ownership data from `usePurchase`
- Integrates with Stellar transaction history instead of duplicating wallet transaction logic
- Reuses existing status color mappings and wallet address formatting utilities for UI consistency
- Supports all buyer transaction pages to ensure complete purchase history correlation
- Preserves existing dashboard architecture while introducing a dedicated content-centric purchase experience

Verification:
- Verified owned courses and books render correctly with appropriate Watch/Read actions
- Confirmed paid purchases display complete Stellar receipt details with working explorer links
- Validated free enrollments display a "Free Enrollment" state without broken receipts
- Confirmed loading, empty, retry, and error states function correctly
- `npm run lint` passes (pre-existing warnings only)
- `npm run build` introduces no new build issues (existing Firebase/Firestore issues are unrelated)
ο»Ώfeat(purchases): add My Purchases library with Stellar payment receipts
added prayer times and Hijri date widget to dashboard (Closes #133)
unified saved hub and bookmark refactor (Closes #132)
@vercel

vercel Bot commented Jul 24, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dnb-frontend Ready Ready Preview, Comment Jul 24, 2026 1:18am

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR adds saved, purchases, and earnings dashboard routes; a prayer-times widget; reusable bookmark state and controls; and PWA support with manifest metadata, install prompting, service-worker caching, offline navigation, and logout cache cleanup.

Changes

Dashboard feature additions

Layer / File(s) Summary
Unified bookmarks and saved hub
hooks/useBookmarkCore.js, hooks/useBookmark.js, hooks/useBookBookmark.js, components/atoms/BookmarkButton.jsx, components/molecules/dashboard/cards/*, app/dashboard/saved/page.jsx, components/molecules/dashboard/nav-routers.jsx
Course and book bookmarks now share optimistic state management and a reusable control, while the Saved page loads, filters, and restores bookmarked items.
Purchases data and receipts
hooks/usePurchases.js, app/dashboard/purchases/page.jsx, components/molecules/dashboard/nav-routers.jsx
Owned courses and books are loaded with buyer transactions, and the purchases page provides tabs, loading/error/empty states, cards, and receipt details.
Earnings analytics dashboard
hooks/useEarnings.js, app/dashboard/earnings/page.jsx, components/molecules/dashboard/nav-routers.jsx
Creator transactions are aggregated into revenue metrics, charts, top items, balances, and status breakdowns rendered by the earnings route.
Prayer times dashboard widget
lib/services/prayer-times.js, components/organisms/dashboard/PrayerTimesWidget.jsx, app/dashboard/page.jsx
Prayer timings are fetched or cached by location, with Hijri dates, next-prayer countdowns, location controls, and dashboard placement.

PWA and offline support

Layer / File(s) Summary
PWA metadata and runtime caching
.npmrc, app/layout.js, app/manifest.js, next.config.mjs, package.json, app/sw.js, public/sw.js, public/swe-worker-*.js, app/offline/page.jsx, app/account/settings/page.jsx, hooks/useAuth.js, README.md
PWA metadata, Serwist integration, runtime caching, offline navigation, install prompting, logout cache cleanup, and screenshot documentation are added.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

  • Issue 132 β€” The Saved hub, shared bookmark core, optimistic toggles, reusable button, and dashboard navigation align directly with this PR.

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Dashboard
  participant PrayerTimesWidget
  participant PrayerTimesService
  participant AladhanAPI
  Dashboard->>PrayerTimesWidget: Render widget
  PrayerTimesWidget->>PrayerTimesService: Load location and timings
  PrayerTimesService->>AladhanAPI: Request daily timings
  AladhanAPI-->>PrayerTimesService: Return timings and dates
  PrayerTimesService-->>PrayerTimesWidget: Return normalized data
  PrayerTimesWidget-->>Dashboard: Render prayers and countdown
Loading
πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (4 passed)
Check name Status Explanation
Description Check βœ… Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check βœ… Passed The title is related to the PR, though it only covers the saved hub/bookmark refactor and omits the broader PWA and dashboard work.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

Comment @coderabbitai help to get the list of available commands.

@zeemscript
zeemscript merged commit 0aecf70 into main Jul 24, 2026
4 of 5 checks passed

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 12

🧹 Nitpick comments (5)
app/dashboard/saved/page.jsx (1)

108-134: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Optional: give the tab toggle proper tab semantics for screen readers.

These two buttons behave as a tab switcher, but to assistive tech they read as two independent buttons. Adding role="tablist" on the container plus role="tab" / aria-selected on each button lets screen-reader users perceive the selected state and grouping. Keyboard operation already works since these are real <button>s, so this is a low-risk enhancement rather than a blocker.

β™Ώ Suggested ARIA wiring
-        <div className="flex gap-2 bg-background/60 p-1.5 rounded-full border shadow-sm">
+        <div role="tablist" aria-label="Saved items" className="flex gap-2 bg-background/60 p-1.5 rounded-full border shadow-sm">
           <button
             type="button"
+            role="tab"
+            aria-selected={activeTab === "courses"}
             onClick={() => setActiveTab("courses")}

Apply the same role="tab" / aria-selected to the Books button.

πŸ€– 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/dashboard/saved/page.jsx` around lines 108 - 134, Add tab semantics to
the active tab switcher: mark the wrapping container as a tablist and apply
role="tab" with aria-selected reflecting activeTab to both the Courses and Books
buttons. Preserve the existing button behavior and styling.
components/organisms/dashboard/PrayerTimesWidget.jsx (1)

148-167: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Prefer inline error UI over blocking alert() for GPS failures.

alert() blocks the main thread and is jarring/inconsistent with the rest of the app's styled feedback (e.g., the error state block used elsewhere in this same component). Consider setting a small inline message near the "Use Auto GPS" button instead.

πŸ€– 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 `@components/organisms/dashboard/PrayerTimesWidget.jsx` around lines 148 - 167,
Update the GPS failure callback in handleAutoDetectGPS to set the component’s
existing error state instead of calling alert(). Render or reuse the established
inline error UI near the β€œUse Auto GPS” button, preserving the current
geolocation success flow.
app/offline/page.jsx (1)

9-21: 🎯 Functional Correctness | πŸ”΅ Trivial | ⚑ Quick win

Hide decorative icon from assistive tech.

This <svg> is purely decorative (the heading already conveys "You're Offline"). Without aria-hidden="true", some screen readers may announce it as an unlabeled graphic.

β™Ώ Proposed fix
             <svg
               className="h-10 w-10 text-accent"
               fill="none"
               viewBox="0 0 24 24"
               stroke="currentColor"
               strokeWidth={1.5}
+              aria-hidden="true"
             >
πŸ€– 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/offline/page.jsx` around lines 9 - 21, Add aria-hidden="true" to the
decorative svg element in the offline page, leaving its visual styling and
existing path content unchanged so assistive technologies skip it.

Source: Path instructions

app/sw.js (1)

117-130: 🎯 Functional Correctness | πŸ”΅ Trivial | πŸ’€ Low value

HTML/navigation detection via Content-Type request header is unreliable.

GET navigation requests don't carry a Content-Type request header (it describes the body's media type; navigations have no body). Browsers instead expose navigation intent via Request.mode === "navigate" (surfaced server-side as Sec-Fetch-Mode: navigate). As written, this matcher will likely never match real page navigations, so they silently fall through to the same-origin-others bucket below instead of pages.

Current impact is limited since both buckets share identical maxEntries/maxAgeSeconds, but if you ever tune them differently the "pages" cache would stay empty.

♻️ Proposed fix
     {
-      matcher: ({ request, url, sameOrigin }) =>
-        request.headers.get("Content-Type")?.includes("text/html") &&
-        sameOrigin,
+      matcher: ({ request, sameOrigin }) =>
+        sameOrigin && request.mode === "navigate",
       handler: new NetworkFirst({
         cacheName: "pages",
πŸ€– 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/sw.js` around lines 117 - 130, Update the matcher associated with the
NetworkFirst β€œpages” cache to identify same-origin navigations using
request.mode === "navigate" instead of inspecting the request Content-Type
header. Preserve the existing sameOrigin requirement and leave the cache
configuration unchanged.
public/sw.js (1)

1-1: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Generated Serwist build artifacts checked into git, with evidence of a Windows-only path bug. Both files are output of the withSerwistInit({ swSrc: "app/sw.js", swDest: "public/sw.js", ... }) pipeline in next.config.mjs and get fully regenerated on every production build β€” they shouldn't need to be committed. The precache manifest baked into public/sw.js contains backslash-separated URLs (e.g. /icons\icon-192x192-maskable.png), which is a giveaway that this specific commit was generated on a Windows machine; if this stale copy is ever served without a fresh build (partial deploys, CDN edge cases, etc.), those asset paths are invalid and precaching would fail for them.

  • public/sw.js#L1-L1: add this generated file to .gitignore and let CI/build regenerate it on every deploy instead of committing it.
  • public/swe-worker-f61931bc2770d10b.js#L1-L1: same β€” gitignore this Serwist-generated worker chunk rather than committing the build output.
πŸ€– 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 `@public/sw.js` at line 1, Generated Serwist service-worker artifacts are
committed and contain machine-specific precache paths; ignore both generated
outputs and remove them from version control. Update .gitignore to exclude
public/sw.js and public/swe-worker-f61931bc2770d10b.js, with no direct source
changes required in either generated file; ensure the withSerwistInit build
regenerates them during deployment.
πŸ€– Prompt for all review comments with 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.

Inline comments:
In `@app/account/settings/page.jsx`:
- Around line 62-70: Update handleInstall so installable is set to false after
any userChoice outcome, not only when the outcome is "accepted"; continue
clearing deferredPrompt afterward so dismissing the native prompt cannot leave
the banner visible with no deferred prompt.

In `@app/layout.js`:
- Line 21: Update the layout metadata manifest value from β€œ/manifest” to
β€œ/manifest.webmanifest” so it references the route served by app/manifest.js and
preserves valid PWA installation metadata.

In `@components/organisms/dashboard/PrayerTimesWidget.jsx`:
- Around line 199-208: Update the loading skeleton grid in PrayerTimesWidget to
use the same responsive column classes as the rendered prayer cards: two columns
by default, three at sm, and five at md. Keep the five skeleton items and
existing styling unchanged.
- Around line 49-97: Guard the initial-load effect in the component containing
loadData so its initialization logic executes only once, preventing
stored-location updates from retriggering it through the loadData dependency.
Preserve the existing stored-location, geolocation, and fallback selection
behavior, while keeping loadData’s location-dependent behavior unchanged.
- Around line 49-65: Update the loadData callback in PrayerTimesWidget so any
response with isFallback: true sets the error state instead of being accepted by
setData, regardless of whether activeLocation exists. Preserve the existing
handling for valid responses and thrown errors, and avoid rendering fallback
prayer times as successful location-specific data.

In `@hooks/useAuth.js`:
- Around line 39-49: Update the logout cache-cleanup logic in the shown
window/caches block to delete all runtime caches, including explicitly named
caches such as "pages", instead of relying only on the "serwist-" prefix and
"book-previews" match. Preserve the existing asynchronous cleanup flow and
ensure every current service-worker runtime cache is removed on logout.

In `@hooks/useEarnings.js`:
- Around line 135-152: Update the last-month filter in the useMemo revenue
calculation to include transactions before startOfThisMonth, removing the
endOfLastMonth dependency from getLastMonthRange and its destructuring. Preserve
the existing startOfLastMonth lower bound and month-over-month calculation.

In `@lib/services/prayer-times.js`:
- Around line 90-124: Update the coordinate checks used to build locationKey and
select the API request in the surrounding prayer-times flow so valid zero
latitude or longitude values are accepted. Test coordinate presence explicitly
rather than relying on truthiness, while preserving the existing city/default
fallback behavior when either coordinate is absent.
- Around line 126-129: Update the Aladhan fetch flow in the surrounding
prayer-times function to use an AbortController with a finite timeout, pass its
signal to fetch, and clear the timeout after completion. Ensure timeout or abort
errors follow the existing catch/fallback path so loading cannot remain stuck on
a hanging request.
- Around line 213-224: Update getNextPrayerInfo to stop converting the
locale-formatted string with new Date(tzStr), which is engine-dependent and can
produce Invalid Date. Derive the timezone-adjusted date using a timezone-safe
Intl.DateTimeFormat(...).formatToParts approach or the project’s existing
timezone helper, while preserving the current fallback to the local Date when
timezone handling fails.

In `@next.config.mjs`:
- Around line 3-9: Update the withSerwist configuration to set reloadOnOnline to
false, preventing reconnect-triggered reloads while preserving the other
service-worker options.

In `@package.json`:
- Line 41: Add serwist as a direct devDependency in package.json alongside
`@serwist/next`, so the app/sw.js import resolves from an explicitly declared
package during clean installs.

---

Nitpick comments:
In `@app/dashboard/saved/page.jsx`:
- Around line 108-134: Add tab semantics to the active tab switcher: mark the
wrapping container as a tablist and apply role="tab" with aria-selected
reflecting activeTab to both the Courses and Books buttons. Preserve the
existing button behavior and styling.

In `@app/offline/page.jsx`:
- Around line 9-21: Add aria-hidden="true" to the decorative svg element in the
offline page, leaving its visual styling and existing path content unchanged so
assistive technologies skip it.

In `@app/sw.js`:
- Around line 117-130: Update the matcher associated with the NetworkFirst
β€œpages” cache to identify same-origin navigations using request.mode ===
"navigate" instead of inspecting the request Content-Type header. Preserve the
existing sameOrigin requirement and leave the cache configuration unchanged.

In `@components/organisms/dashboard/PrayerTimesWidget.jsx`:
- Around line 148-167: Update the GPS failure callback in handleAutoDetectGPS to
set the component’s existing error state instead of calling alert(). Render or
reuse the established inline error UI near the β€œUse Auto GPS” button, preserving
the current geolocation success flow.

In `@public/sw.js`:
- Line 1: Generated Serwist service-worker artifacts are committed and contain
machine-specific precache paths; ignore both generated outputs and remove them
from version control. Update .gitignore to exclude public/sw.js and
public/swe-worker-f61931bc2770d10b.js, with no direct source changes required in
either generated file; ensure the withSerwistInit build regenerates them during
deployment.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 873d34aa-9fec-43c2-8989-d3fbe82ed011

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between c76e032 and f8edf20.

β›” Files ignored due to path filters (11)
  • docs/screenshots/courses.png is excluded by !**/*.png
  • docs/screenshots/dashboard.png is excluded by !**/*.png
  • docs/screenshots/landing.png is excluded by !**/*.png
  • docs/screenshots/library.png is excluded by !**/*.png
  • docs/screenshots/login.png is excluded by !**/*.png
  • docs/screenshots/wallet.png is excluded by !**/*.png
  • package-lock.json is excluded by !**/package-lock.json
  • public/icons/icon-192x192-maskable.png is excluded by !**/*.png
  • public/icons/icon-192x192.png is excluded by !**/*.png
  • public/icons/icon-512x512-maskable.png is excluded by !**/*.png
  • public/icons/icon-512x512.png is excluded by !**/*.png
πŸ“’ Files selected for processing (27)
  • .npmrc
  • README.md
  • app/account/settings/page.jsx
  • app/dashboard/earnings/page.jsx
  • app/dashboard/page.jsx
  • app/dashboard/purchases/page.jsx
  • app/dashboard/saved/page.jsx
  • app/layout.js
  • app/manifest.js
  • app/offline/page.jsx
  • app/sw.js
  • components/atoms/BookmarkButton.jsx
  • components/molecules/dashboard/cards/courseCard.jsx
  • components/molecules/dashboard/cards/libraryCard.jsx
  • components/molecules/dashboard/nav-routers.jsx
  • components/organisms/dashboard/PrayerTimesWidget.jsx
  • hooks/useAuth.js
  • hooks/useBookBookmark.js
  • hooks/useBookmark.js
  • hooks/useBookmarkCore.js
  • hooks/useEarnings.js
  • hooks/usePurchases.js
  • lib/services/prayer-times.js
  • next.config.mjs
  • package.json
  • public/sw.js
  • public/swe-worker-f61931bc2770d10b.js

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

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 becomes a dead end after the user dismisses the native prompt.

installable only resets to false on "accepted". On dismiss, deferredPrompt is cleared but the banner (which gates on installable) stays visible β€” clicking "Install" again then silently no-ops since handleInstall early-returns when deferredPrompt is null.

πŸ› 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 set to false after any userChoice outcome, not only when the
outcome is "accepted"; continue clearing deferredPrompt afterward so dismissing
the native prompt cannot leave the banner visible with no deferred prompt.

Comment thread app/layout.js
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",

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 'Files of interest:\n'
git ls-files app/layout.js app/manifest.js public/sw.js 2>/dev/null || true

printf '\nLayout snippet:\n'
sed -n '1,120p' app/layout.js 2>/dev/null || true

printf '\nManifest file:\n'
sed -n '1,120p' app/manifest.js 2>/dev/null || true

printf '\nReferences to manifest path:\n'
rg -n 'manifest\.webmanifest|manifest: "/manifest"|manifest: "/manifest\.webmanifest"|/manifest"' app public 2>/dev/null || true

Repository: Deen-Bridge/dnb-frontend

Length of output: 34411


Point manifest at /manifest.webmanifest
app/manifest.js is served at /manifest.webmanifest; manifest: "/manifest" emits a broken link and can block PWA install.

πŸ› 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",
manifest: "/manifest.webmanifest",
πŸ€– 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` at line 21, Update the layout metadata manifest value from
β€œ/manifest” to β€œ/manifest.webmanifest” so it references the route served by
app/manifest.js and preserves valid PWA installation metadata.

Comment on lines +49 to +65
const loadData = useCallback(async (locObj) => {
setLoading(true);
setError(false);
try {
const activeLocation = locObj || location || (user?.country ? { city: user.country } : null);
const res = await fetchPrayerTimes(activeLocation);
if (!res || (res.isFallback && !activeLocation)) {
setError(true);
} else {
setData(res);
}
} catch (_err) {
setError(true);
} finally {
setLoading(false);
}
}, [location, user?.country]);

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

isFallback is silently swallowed once a location is set, hiding real API failures.

setError(true) only fires when res.isFallback && !activeLocation. Once the user has a real activeLocation (stored, geolocated, or manually entered) and the Aladhan request fails for any reason (network blip, bad city name, API outage), fetchPrayerTimes (Line 181-204 in lib/services/prayer-times.js) still returns a resolved object with isFallback: true and generic hardcoded times β€” and this branch happily calls setData(res), rendering those made-up times as if they were the user's real, location-specific prayer schedule with no indication anything went wrong.

πŸ”§ Proposed fix β€” surface the fallback state instead of discarding it
       const res = await fetchPrayerTimes(activeLocation);
-      if (!res || (res.isFallback && !activeLocation)) {
-        setError(true);
-      } else {
-        setData(res);
-      }
+      if (!res) {
+        setError(true);
+      } else {
+        setData(res);
+        // consider surfacing res.isFallback in the UI (e.g. a subtle "estimated times" badge)
+      }
🧰 Tools
πŸͺ› ast-grep (0.44.1)

[warning] 57-57: Avoid using the initial state variable in setState
Context: setData(res)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

πŸ€– 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 `@components/organisms/dashboard/PrayerTimesWidget.jsx` around lines 49 - 65,
Update the loadData callback in PrayerTimesWidget so any response with
isFallback: true sets the error state instead of being accepted by setData,
regardless of whether activeLocation exists. Preserve the existing handling for
valid responses and thrown errors, and avoid rendering fallback prayer times as
successful location-specific data.

Comment on lines +49 to +97
const loadData = useCallback(async (locObj) => {
setLoading(true);
setError(false);
try {
const activeLocation = locObj || location || (user?.country ? { city: user.country } : null);
const res = await fetchPrayerTimes(activeLocation);
if (!res || (res.isFallback && !activeLocation)) {
setError(true);
} else {
setData(res);
}
} catch (_err) {
setError(true);
} finally {
setLoading(false);
}
}, [location, user?.country]);

// Initial load & Geolocation auto-detection
useEffect(() => {
const stored = getStoredLocation();
if (stored) {
setLocation(stored);
loadData(stored);
} else if (typeof window !== "undefined" && navigator.geolocation) {
// Try non-blocking geolocation
navigator.geolocation.getCurrentPosition(
(pos) => {
const geoLoc = {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
name: "Current Location",
};
setLocation(geoLoc);
setStoredLocation(geoLoc);
loadData(geoLoc);
},
(_geoErr) => {
// Fallback to profile country or default
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
},
{ timeout: 5000 }
);
} else {
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
}
}, [user?.country, loadData]);

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.

🩺 Stability & Availability | πŸ”΄ Critical | ⚑ Quick win

Infinite effect loop when a stored location exists.

getStoredLocation() (in lib/services/prayer-times.js) parses JSON on every call, returning a brand-new object reference each time even when the content is identical. Here's the loop this creates:

  1. Effect runs β†’ setLocation(stored) with a fresh object β†’ location state reference changes.
  2. loadData is a useCallback with location in its deps (Line 65) β†’ its identity changes too.
  3. The effect's dep array [user?.country, loadData] now differs β†’ effect re-runs.
  4. Repeat forever.

Any user with a previously saved location (the common case after first use) will trigger continuous re-renders and repeated loadData calls indefinitely β€” this is a genuine runaway effect, not just a wasted render.

πŸ› Proposed fix β€” guard the initial-load effect so it only runs its body once
+  const didInitRef = useRef(false);
+
   // Initial load & Geolocation auto-detection
   useEffect(() => {
+    if (didInitRef.current) return;
+    didInitRef.current = true;
     const stored = getStoredLocation();
     if (stored) {
       setLocation(stored);
       loadData(stored);
     } else if (typeof window !== "undefined" && navigator.geolocation) {
πŸ“ 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 loadData = useCallback(async (locObj) => {
setLoading(true);
setError(false);
try {
const activeLocation = locObj || location || (user?.country ? { city: user.country } : null);
const res = await fetchPrayerTimes(activeLocation);
if (!res || (res.isFallback && !activeLocation)) {
setError(true);
} else {
setData(res);
}
} catch (_err) {
setError(true);
} finally {
setLoading(false);
}
}, [location, user?.country]);
// Initial load & Geolocation auto-detection
useEffect(() => {
const stored = getStoredLocation();
if (stored) {
setLocation(stored);
loadData(stored);
} else if (typeof window !== "undefined" && navigator.geolocation) {
// Try non-blocking geolocation
navigator.geolocation.getCurrentPosition(
(pos) => {
const geoLoc = {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
name: "Current Location",
};
setLocation(geoLoc);
setStoredLocation(geoLoc);
loadData(geoLoc);
},
(_geoErr) => {
// Fallback to profile country or default
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
},
{ timeout: 5000 }
);
} else {
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
}
}, [user?.country, loadData]);
const didInitRef = useRef(false);
// Initial load & Geolocation auto-detection
useEffect(() => {
if (didInitRef.current) return;
didInitRef.current = true;
const stored = getStoredLocation();
if (stored) {
setLocation(stored);
loadData(stored);
} else if (typeof window !== "undefined" && navigator.geolocation) {
// Try non-blocking geolocation
navigator.geolocation.getCurrentPosition(
(pos) => {
const geoLoc = {
lat: pos.coords.latitude,
lng: pos.coords.longitude,
name: "Current Location",
};
setLocation(geoLoc);
setStoredLocation(geoLoc);
loadData(geoLoc);
},
(_geoErr) => {
// Fallback to profile country or default
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
},
{ timeout: 5000 }
);
} else {
const fallbackLoc = user?.country ? { city: user.country } : null;
loadData(fallbackLoc);
}
}, [user?.country, loadData]);
🧰 Tools
πŸͺ› ast-grep (0.44.1)

[warning] 57-57: Avoid using the initial state variable in setState
Context: setData(res)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[warning] 70-70: Avoid using the initial state variable in setState
Context: setLocation(stored)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[warning] 81-81: Avoid using the initial state variable in setState
Context: setLocation(geoLoc)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)


[warning] 82-82: Avoid using the initial state variable in setState
Context: setStoredLocation(geoLoc)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

πŸ€– 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 `@components/organisms/dashboard/PrayerTimesWidget.jsx` around lines 49 - 97,
Guard the initial-load effect in the component containing loadData so its
initialization logic executes only once, preventing stored-location updates from
retriggering it through the loadData dependency. Preserve the existing
stored-location, geolocation, and fallback selection behavior, while keeping
loadData’s location-dependent behavior unchanged.

Comment on lines +199 to +208
{/* Loading State */}
{loading ? (
<div className="animate-pulse space-y-3 py-4">
<div className="h-16 bg-muted rounded-xl w-full" />
<div className="grid grid-cols-5 gap-2">
{[...Array(5)].map((_, i) => (
<div key={`skel-pr-${i}`} className="h-20 bg-muted rounded-xl" />
))}
</div>
</div>

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.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Loading skeleton column count doesn't match the real content's responsive breakpoints.

The skeleton grid is hardcoded to grid-cols-5 (Line 203) while the actual prayer cards (Line 251) use grid-cols-2 sm:grid-cols-3 md:grid-cols-5. On mobile this causes a visible layout shift/jump when loading finishes.

🎨 Proposed fix
-            <div className="grid grid-cols-5 gap-2">
+            <div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
πŸ“ 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
{/* Loading State */}
{loading ? (
<div className="animate-pulse space-y-3 py-4">
<div className="h-16 bg-muted rounded-xl w-full" />
<div className="grid grid-cols-5 gap-2">
{[...Array(5)].map((_, i) => (
<div key={`skel-pr-${i}`} className="h-20 bg-muted rounded-xl" />
))}
</div>
</div>
{/* Loading State */}
{loading ? (
<div className="animate-pulse space-y-3 py-4">
<div className="h-16 bg-muted rounded-xl w-full" />
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-2">
{[...Array(5)].map((_, i) => (
<div key={`skel-pr-${i}`} className="h-20 bg-muted rounded-xl" />
))}
</div>
</div>
πŸ€– 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 `@components/organisms/dashboard/PrayerTimesWidget.jsx` around lines 199 - 208,
Update the loading skeleton grid in PrayerTimesWidget to use the same responsive
column classes as the rendered prayer cards: two columns by default, three at
sm, and five at md. Keep the five skeleton items and existing styling unchanged.

Comment on lines +90 to +124
const locationKey = location?.lat && location?.lng
? `${location.lat.toFixed(2)},${location.lng.toFixed(2)}`
: (location?.city || location?.name || "default");

// Check cache
if (typeof window !== "undefined") {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached);
if (parsed.dateKey === todayStr && parsed.locationKey === locationKey && parsed.data) {
return parsed.data;
}
}
} catch (_e) {
// Ignore cache read errors
}
}

let apiUrl = "";
let locationName = "Current Location";

if (location?.lat && location?.lng) {
const timestamp = Math.floor(Date.now() / 1000);
apiUrl = `https://api.aladhan.com/v1/timings/${timestamp}?latitude=${location.lat}&longitude=${location.lng}&method=2`;
locationName = location.name || `${location.lat.toFixed(2)}Β°, ${location.lng.toFixed(2)}Β°`;
} else if (location?.city) {
const country = location.country || "";
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent(location.city)}&country=${encodeURIComponent(country)}&method=2`;
locationName = location.country ? `${location.city}, ${location.country}` : location.city;
} else {
// Default fallback city (Mecca / Saudi Arabia)
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent("Mecca")}&country=${encodeURIComponent("Saudi Arabia")}&method=2`;
locationName = "Mecca, Saudi Arabia";
}

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

Falsy-check bug: lat/lng of 0 silently discards valid GPS coordinates.

location?.lat && location?.lng (Lines 90, 112) treats an exact 0 value as falsy. A user located on the equator (lat: 0) or on the prime meridian (lng: 0) will fall through to the city/default branch, silently losing their real GPS-based location for both the cache key and the API request built later.

πŸ› Proposed fix
-  const locationKey = location?.lat && location?.lng
+  const hasCoords = typeof location?.lat === "number" && typeof location?.lng === "number";
+  const locationKey = hasCoords
     ? `${location.lat.toFixed(2)},${location.lng.toFixed(2)}`
     : (location?.city || location?.name || "default");
-  if (location?.lat && location?.lng) {
+  if (hasCoords) {
πŸ“ 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 locationKey = location?.lat && location?.lng
? `${location.lat.toFixed(2)},${location.lng.toFixed(2)}`
: (location?.city || location?.name || "default");
// Check cache
if (typeof window !== "undefined") {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached);
if (parsed.dateKey === todayStr && parsed.locationKey === locationKey && parsed.data) {
return parsed.data;
}
}
} catch (_e) {
// Ignore cache read errors
}
}
let apiUrl = "";
let locationName = "Current Location";
if (location?.lat && location?.lng) {
const timestamp = Math.floor(Date.now() / 1000);
apiUrl = `https://api.aladhan.com/v1/timings/${timestamp}?latitude=${location.lat}&longitude=${location.lng}&method=2`;
locationName = location.name || `${location.lat.toFixed(2)}Β°, ${location.lng.toFixed(2)}Β°`;
} else if (location?.city) {
const country = location.country || "";
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent(location.city)}&country=${encodeURIComponent(country)}&method=2`;
locationName = location.country ? `${location.city}, ${location.country}` : location.city;
} else {
// Default fallback city (Mecca / Saudi Arabia)
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent("Mecca")}&country=${encodeURIComponent("Saudi Arabia")}&method=2`;
locationName = "Mecca, Saudi Arabia";
}
const hasCoords = typeof location?.lat === "number" && typeof location?.lng === "number";
const locationKey = hasCoords
? `${location.lat.toFixed(2)},${location.lng.toFixed(2)}`
: (location?.city || location?.name || "default");
// Check cache
if (typeof window !== "undefined") {
try {
const cached = localStorage.getItem(CACHE_KEY);
if (cached) {
const parsed = JSON.parse(cached);
if (parsed.dateKey === todayStr && parsed.locationKey === locationKey && parsed.data) {
return parsed.data;
}
}
} catch (_e) {
// Ignore cache read errors
}
}
let apiUrl = "";
let locationName = "Current Location";
if (hasCoords) {
const timestamp = Math.floor(Date.now() / 1000);
apiUrl = `https://api.aladhan.com/v1/timings/${timestamp}?latitude=${location.lat}&longitude=${location.lng}&method=2`;
locationName = location.name || `${location.lat.toFixed(2)}Β°, ${location.lng.toFixed(2)}Β°`;
} else if (location?.city) {
const country = location.country || "";
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent(location.city)}&country=${encodeURIComponent(country)}&method=2`;
locationName = location.country ? `${location.city}, ${location.country}` : location.city;
} else {
// Default fallback city (Mecca / Saudi Arabia)
apiUrl = `https://api.aladhan.com/v1/timingsByCity?city=${encodeURIComponent("Mecca")}&country=${encodeURIComponent("Saudi Arabia")}&method=2`;
locationName = "Mecca, Saudi Arabia";
}
πŸ€– 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 `@lib/services/prayer-times.js` around lines 90 - 124, Update the coordinate
checks used to build locationKey and select the API request in the surrounding
prayer-times flow so valid zero latitude or longitude values are accepted. Test
coordinate presence explicitly rather than relying on truthiness, while
preserving the existing city/default fallback behavior when either coordinate is
absent.

Comment on lines +126 to +129
try {
const res = await fetch(apiUrl);
if (!res.ok) throw new Error("Failed to fetch prayer times from API");
const json = await res.json();

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.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Add a timeout/abort guard around the Aladhan fetch.

There's no AbortController/timeout here, so a slow or hanging upstream response can leave the widget's loading state stuck far longer than a user will tolerate, with no way to bail out early into the fallback path.

πŸ”§ Proposed fix
   try {
-    const res = await fetch(apiUrl);
+    const controller = new AbortController();
+    const timeoutId = setTimeout(() => controller.abort(), 8000);
+    const res = await fetch(apiUrl, { signal: controller.signal }).finally(() =>
+      clearTimeout(timeoutId)
+    );
     if (!res.ok) throw new Error("Failed to fetch prayer times from API");
πŸ“ 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
try {
const res = await fetch(apiUrl);
if (!res.ok) throw new Error("Failed to fetch prayer times from API");
const json = await res.json();
try {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 8000);
const res = await fetch(apiUrl, { signal: controller.signal }).finally(() =>
clearTimeout(timeoutId)
);
if (!res.ok) throw new Error("Failed to fetch prayer times from API");
const json = await res.json();
πŸ€– 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 `@lib/services/prayer-times.js` around lines 126 - 129, Update the Aladhan
fetch flow in the surrounding prayer-times function to use an AbortController
with a finite timeout, pass its signal to fetch, and clear the timeout after
completion. Ensure timeout or abort errors follow the existing catch/fallback
path so loading cannot remain stuck on a hanging request.

Comment on lines +213 to +224
export function getNextPrayerInfo(timings, timezone = null) {
if (!timings) return { nextPrayerName: "Fajr", remainingSeconds: 0, isTomorrow: false };

let nowInTz = new Date();
if (timezone) {
try {
const tzStr = new Date().toLocaleString("en-US", { timeZone: timezone });
nowInTz = new Date(tzStr);
} catch (_e) {
nowInTz = new Date();
}
}

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

🧩 Analysis chain

🌐 Web query:

Is parsing the output of Date.prototype.toLocaleString back into a Date object officially specified or safe in modern JS engines?

πŸ’‘ Result:

No, parsing the output of Date.prototype.toLocaleString back into a Date object is not officially specified as safe or reliable in modern JavaScript [1][2][3]. The ECMAScript specification does not guarantee that toLocaleString produces a format that Date.parse or the Date constructor can reliably interpret [1][2][3]. The output of toLocaleString is intentionally implementation-defined to allow for locale-specific formatting variationsβ€”such as different date/time orderings, specialized calendar symbols, non-breaking spaces, or bidirectional control charactersβ€”which are not standardized and can vary between browsers, operating systems, and even different versions of the same environment [4][5]. Key points regarding this practice: - Implementation-Defined Behavior: The specification explicitly states that while Date.parse may attempt to support various formats, it is not required to handle the strings produced by toLocaleString [1][2][3]. - Lack of Round-trip Guarantee: Unlike toISOString, which is explicitly designed for machine-readable, round-trip serialization of date objects, toLocaleString is intended solely for human-readable display [4][3]. - Lack of Parsing Facilities: The TC39 committee has intentionally excluded parsing facilities from the Internationalization API (ECMA-402) after extensive deliberation, noting that reliable date parsing is inherently complex and prone to ambiguity [6][7]. If you require a format that can be safely parsed back into a Date object, you should use toISOString or a structured, standardized format that is explicitly supported by Date.parse, such as the ISO 8601 subset defined in the ECMAScript specification [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

git ls-files lib/services/prayer-times.js
wc -l lib/services/prayer-times.js
cat -n lib/services/prayer-times.js | sed -n '190,245p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 2366


Avoid parsing toLocaleString() output here. new Date(tzStr) depends on engine-specific date parsing and can become Invalid Date, which turns the remaining-time math into NaN. Use a timezone-safe approach instead, such as Intl.DateTimeFormat(...).formatToParts or a dedicated timezone helper.

πŸ€– 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 `@lib/services/prayer-times.js` around lines 213 - 224, Update
getNextPrayerInfo to stop converting the locale-formatted string with new
Date(tzStr), which is engine-dependent and can produce Invalid Date. Derive the
timezone-adjusted date using a timezone-safe
Intl.DateTimeFormat(...).formatToParts approach or the project’s existing
timezone helper, while preserving the current fallback to the local Date when
timezone handling fails.

Comment thread next.config.mjs
Comment on lines +3 to +9
const withSerwist = withSerwistInit({
swSrc: "app/sw.js",
swDest: "public/sw.js",
disable: process.env.NODE_ENV === "development",
cacheOnNavigation: true,
reloadOnOnline: true,
});

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.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== next.config.mjs =="
cat -n next.config.mjs

echo
echo "== Search for form-related surfaces =="
rg -n --hidden -S "autosave|save on|unsaved|form|textarea|input|messag|profile|settings" app src pages components . \
  -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' || true

Repository: Deen-Bridge/dnb-frontend

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== next.config.mjs =="
cat -n next.config.mjs

echo
echo "== Candidate editor/form files =="
git ls-files \
  "app/**/settings*" \
  "app/**/profile*" \
  "app/**/messages*" \
  "components/**/settings*" \
  "components/**/profile*" \
  "components/**/messages*" \
  "components/**/form*" \
  | sed -n '1,200p'

echo
echo "== Search for autosave / draft persistence / unload handling =="
rg -n -S "autosave|auto-save|draft|persist|localStorage|sessionStorage|beforeunload|visibilitychange|ononline|reloadOnOnline|save.*draft|keep.*draft" \
  app components hooks lib \
  -g '!node_modules' -g '!dist' -g '!build' -g '!coverage' || true

Repository: Deen-Bridge/dnb-frontend

Length of output: 5230


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== outlines =="
ast-grep outline app/account/settings/page.jsx --view expanded || true
echo
ast-grep outline app/account/profile/[profileid]/page.jsx --view expanded || true
echo
ast-grep outline app/dashboard/messages/[room]/page.jsx --view expanded || true
echo
ast-grep outline components/organisms/settings/resetPassword.jsx --view expanded || true
echo
ast-grep outline components/organisms/account/profile/ProfileContent.jsx --view expanded || true
echo
ast-grep outline components/organisms/account/profile/ProfileTabs.jsx --view expanded || true
echo
ast-grep outline components/molecules/messages/isTypingbubbles.jsx --view expanded || true

echo
echo "== settings page excerpt =="
cat -n app/account/settings/page.jsx | sed -n '1,220p'

echo
echo "== profile page excerpt =="
cat -n app/account/profile/[profileid]/page.jsx | sed -n '1,220p'

echo
echo "== messages room page excerpt =="
cat -n app/dashboard/messages/[room]/page.jsx | sed -n '1,220p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 20891


Disable reconnect reloads in next.config.mjs:3-8. reloadOnOnline: true can wipe unsaved settings, profile, and message drafts when a user goes offline and comes back online. Flip it to false unless discarding in-progress edits is intentional.

πŸ€– 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 `@next.config.mjs` around lines 3 - 9, Update the withSerwist configuration to
set reloadOnOnline to false, preventing reconnect-triggered reloads while
preserving the other service-worker options.

Comment thread package.json
"@radix-ui/react-toggle": "^1.1.6",
"@radix-ui/react-toggle-group": "^1.1.7",
"@radix-ui/react-tooltip": "^1.2.4",
"@serwist/next": "^9.5.11",

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n '"serwist"\s*:' package.json

Repository: Deen-Bridge/dnb-frontend

Length of output: 162


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package.json (dependency section) ---'
nl -ba package.json | sed -n '1,140p'

echo
echo '--- app/sw.js import lines ---'
nl -ba app/sw.js | sed -n '1,120p'

Repository: Deen-Bridge/dnb-frontend

Length of output: 244


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- package.json (dependencies/devDependencies) ---'
python3 - <<'PY'
from pathlib import Path
import json
p = Path("package.json")
data = json.loads(p.read_text())
for section in ("dependencies", "devDependencies", "peerDependencies", "optionalDependencies"):
    if section in data:
        print(f"\n[{section}]")
        for k, v in data[section].items():
            print(f"{k}: {v}")
PY

echo
echo '--- app/sw.js (imports) ---'
python3 - <<'PY'
from pathlib import Path
p = Path("app/sw.js")
for i, line in enumerate(p.read_text().splitlines(), 1):
    if "serwist" in line or "import" in line:
        print(f"{i}: {line}")
PY

Repository: Deen-Bridge/dnb-frontend

Length of output: 2433


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- repo files mentioning serwist ---'
python3 - <<'PY'
from pathlib import Path
for path in Path('.').rglob('*'):
    if path.is_file() and path.suffix in {'.json', '.yaml', '.yml', '.js', '.ts', '.mjs', '.cjs'}:
        try:
            text = path.read_text()
        except Exception:
            continue
        if 'serwist' in text:
            print(path)
PY

echo
echo '--- lockfile/package manager hints ---'
python3 - <<'PY'
from pathlib import Path
for name in ["package-lock.json", "pnpm-lock.yaml", "yarn.lock", "bun.lockb"]:
    p = Path(name)
    print(f"{name}: {'yes' if p.exists() else 'no'}")
PY

Repository: Deen-Bridge/dnb-frontend

Length of output: 5479


Add serwist as a direct devDependency
app/sw.js imports from serwist, but package.json only adds @serwist/next. The build is currently depending on a transitive install path, so declare serwist directly to keep the service-worker bundle stable across clean installs.

πŸ€– 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 `@package.json` at line 41, Add serwist as a direct devDependency in
package.json alongside `@serwist/next`, so the app/sw.js import resolves from an
explicitly declared package during clean installs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants