Skip to content

Fix/payment modal cleanup - #166

Merged
zeemscript merged 55 commits into
mainfrom
fix/payment-modal-cleanup
Jul 31, 2026
Merged

Fix/payment modal cleanup#166
zeemscript merged 55 commits into
mainfrom
fix/payment-modal-cleanup

Conversation

@zeemscript

@zeemscript zeemscript commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added public educator profiles with follow, sharing, ratings, and content tabs.
    • Added global command-palette search with recent searches and quick actions.
    • Added course progress tracking, resume/start-over controls, and completion status.
    • Added email verification and Stellar wallet authentication.
    • Added enhanced library filters, sorting, search, and pagination.
    • Added a global community showcase section to the landing page.
  • Bug Fixes

    • Improved wallet, payment, video playback, notifications, authentication, and transaction error handling.
    • Improved responsive layouts, loading states, accessibility, and navigation links.
  • Documentation

    • Expanded environment configuration guidance in the README.

Times-stack and others added 30 commits July 26, 2026 01:02
- course-create-form.jsx: fixed duplicated htmlFor=title on all four
  labels (description/category/price labels pointed at the title
  input instead of their own field); added matching id to
  Input/Textarea; fixed Label id=thumbail/id=file typos to
  htmlFor=thumbnail/file so the upload section labels actually
  associate.
- ComboBox.jsx (CategoryCombobox): added an id prop forwarded to the
  trigger button so the course-category label can associate with it.
- Searchbox.jsx: added a visually-hidden (sr-only) label wired via
  useId, giving the search input an accessible name beyond just a
  placeholder.
- TransactionHistory.jsx: added an aria-label to the icon-only
  explorer link and aria-hidden to its icon.
- ReelActionButton.jsx: added aria-label to the button and
  aria-hidden to the icon span, so icon-only reel actions have an
  accessible name.

StarRate.jsx already had a proper aria-label/role=img for read-only
ratings, so left untouched.

Scope note: this covers the concrete labeling/accessible-name items
from #134 pointers. Contrast audit, skip-to-content link, shell
landmarks, and wiring an automated a11y check are separate, larger
pieces of this issue not yet addressed here.

npm run lint and npm run build both pass.
…progress bars

- Add useCourseProgress hook with localStorage write-behind and throttled API writes
- Add useAllCourseProgress for batched progress on course listing pages
- Extend VidPlayerBox with startTime, onTimeUpdate, and onEnded props
- Add resume/start-over UX on course detail page with progress indicator
- Add progress bars and completion badges to CourseCard for owned courses
- Graceful degradation: works via localStorage when backend endpoints unavailable
- Completion marked at >=90% watched or on player ended event

Closes #108
…tracking

feat(course-progress): add resume playback, completion tracking, and progress bars
… (CodeRabbit)

label holds the visible count (e.g. 42), not an action name, so
aria-label={label} announced a bare number with no context for
like/love/comments/share buttons - or nothing at all where label
was numeric 0. Added a separate accessibleLabel prop used for
aria-label, and set it to Like/Love/Comments/Share at all 8
ReelActionButton call sites (desktop + mobile layouts) in
ReelCard.jsx. label remains the visible count only.
…rding

Revert 137 feat/wallet onboarding
…deRabbit)

- accessibleLabel now folds in the visible count (e.g. "Like, 42")
  instead of just the action name, so screen readers announce both.
- Added a pressed prop to ReelActionButton that renders aria-pressed,
  wired to pressed={viewerLiked}/{viewerLoved} on the like/love
  instances only, so assistive tech can tell whether a reaction is
  currently active. Not applied to comments/share since those are not
  toggle buttons.
- Add /educators/[profileid] public route accessible without authentication
- Add EducatorProfileHeader with avatar, bio, role, stats, follower count
- Add PublicCourseCard, PublicBookCard, PublicSpaceCard (no auth dependencies)
- Add tabbed navigation for courses, books, and spaces
- Add share button with Web Share API and clipboard fallback
- Add auth-aware follow: logged-out users redirect to /login?next=...
- Add privacy guard: shows not-found for missing/private profiles
- Rewire instructor links on courseCard, libraryCard, spaceCard to /educators/[id]
- Add 'View public page' link on authenticated profile page
- Add generateMetadata for SEO (title, description, OpenGraph)

Closes #116
…, and user rejection

- Add lib/stellar/stellarErrors.js: reusable error mapping utility
- StellarProvider: wallet detection, network mismatch check, validateForPayment()
- PaymentModal: no-wallet install prompts, network mismatch warning, trustline/balance checks
- WalletConnectButton: install prompts when no extension, network mismatch badge
- useStellarPayment: graceful user rejection handling
ο»Ώfix: a11y label associations and accessible names (#134)
…storefront

feat(educators): add public educator storefront pages
- Replace hardcoded files.vidstack.io demo subtitles/thumbnails with
  real course captions, sourced only from data.subtitles/data.chapters
- Add chapter markers (VTT or array) with graceful degrade when absent
- Persist playback rate/volume/muted to localStorage across courses
- Add clear error state for missing/broken video instead of blank player
- Add discoverable keyboard-shortcuts help and a focusable, labelled player
- Lazy-load the player component to keep Vidstack off the initial bundle

Progress/resume tracking intentionally left to #108.
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
- Await params and cookies() in app/api/books/[bookId]/preview/route.js
- Await params in 4 server component pages:
  - dashboard/courses/[courseId]/page.jsx
  - dashboard/library/[bookid]/page.jsx
  - dashboard/library/read/[bookid]/page.jsx
  - dashboard/spaces/[spacesid]/page.jsx
- Use React.use(params) in client component dashboard/search/[searchparam]/page.jsx
fix: await dynamic APIs for Next.js 15 compatibility
- Error vs empty state: getTransactionHistory now returns error message;
  TransactionHistory tracks error state and renders an inline error card
  with 'Try Again' button instead of misleading 'No transactions found'
- Role page reset: handleRoleChange resets pagination to page 1 so
  switching buyer/creator always fetches the first page
- Header always mounted: role select and title stay interactive during
  loading; only the table area skeletons; pagination buttons disabled
  while fetch is in flight
- exhaustive-deps: fetchTransactions wrapped in useCallback with proper
  deps; effect depends only on fetchTransactions
Throw error if network request fails in development mode.
…ror-handling

fix: error handling, role reset, and loading UX in TransactionHistory
…ersal-search

feat: implemented global command palette and universal search
- wire StatsOverview to use live data from useStats()
- add loading skeletons, error state, and retry support for stats
- correct dashboard icon mapping for all stat cards
- fetch and display real upcoming sessions from the backend
- add loading and empty states for upcoming sessions
- link "View Upcomings" to the dashboard spaces page
- remove hardcoded learning progress chart data and misleading metrics
- add honest placeholder state pending backend activity data
- prepare chart with distinct series configuration for future monthly analytics
- clean up unused imports and ensure successful build
feat(dashboard): replace placeholder widgets with real dashboard data
IamOluwatoyin and others added 25 commits July 28, 2026 14:08
- replace hand-rolled portal Modal with Radix Dialog primitives
- add role=dialog, aria-modal, and aria-labelledby via DialogPrimitive
- inherit Radix focus trap, focus restore, and scroll lock
- overlay click and Escape close now work as expected
- add aria-label=Close to the close button
- honor className prop on the content container (fixes Notybell sizing)
- remove AnimatePresence wrapper from Notybell (Radix handles lifecycle)
- clean up unused imports
- preserve existing visual style (bg-accent header, scrollable body)
- lint and build pass cleanly
fix(modal): reimplement Modal over Radix Dialog for full accessibility
…dead-components

chore(deps): remove unused dependencies and dead components
…agination

feat(library): add search, filters, sorting, and pagination
- Rewrite .env.example with only the variables the code reads; remove
  NEXT_PUBLIC_CLOUDINARY_API_SECRET, _API_KEY, _URL, DNB_API_URL,
  NEXT_PUBLIC_SOCKET_URL
- Add lib/config/env.js with Zod schema that validates URLs, constrains
  STELLAR_NETWORK to testnet|mainnet, rejects NEXT_PUBLIC_*SECRET vars,
  and fails build with aggregated error messages
- Source Firebase config from env vars with current values as defaults
- Route all process.env reads through the validated config object
- Update README configuration table with all variables and required column
The Zod schema required NEXT_PUBLIC_CLOUDINARY_CLOUD_NAME, causing the
build to fail when the variable is not set in CI. The runtime code in
cloudinaryUpload.js already validates the cloud name and throws a clear
error, so the schema should allow undefined and warn at access time.
…validation

feat: secure and validate environment configuration
…tencies

- Create AuthProvider (components/providers/AuthProvider.jsx) that reads cookies once and owns user/isAuthenticated/loading/logout/refreshUser state via React context
- Rewrite hooks/useAuth.js to consume the context (same return shape preserved)
- Mount AuthProvider in app/layout.js above StellarProvider
- Normalize all cookie writes to use { expires: 1, path: '/' } in login, signup, and refreshUser
- Wrap userInfo JSON.parse in try/catch; on failure clear cookies and treat as logged out
- Add module-level callback mechanism so login/signup update context state immediately after setting cookies, eliminating the need for setTimeout workarounds

Fixes #73
- Destructure cancelPayment from useStellarPayment and call it when
  modal closes with a pending, unsubmitted transaction (confirm step
  or Back button)
- Modify executePayment to return { success, cancelled, data } so the
  modal can distinguish wallet rejection from other failures
- On wallet rejection: cancel the pending backend transaction and
  return user to preview step instead of leaving the tx dangling
- Block overlay click and Escape dismissal during processing
  (onPointerDownOutside / onEscapeKeyDown preventDefault)
- Update processing text to 'Waiting for wallet confirmation…'
- Add closingRef guard to make handleClose safe for multiple calls
- Back button now cancels the pending tx, ensuring initiate-back-
  initiate does not create orphaned duplicate transactions

Fixes #76
- book-create-form.jsx: RHF + zod schema (title, description, category,
  price); label/input associations fixed via shadcn Form primitives;
  file validation using validateFile helper; currency changed from
  NGN to USDC; removed alert() calls, replaced with inline errors + toast
- course-create-form.jsx: RHF + zod schema; CategoryCombobox integrated
  via Controller; file validation on thumbnail/video; currency to USDC
- space-create-form.jsx: RHF + zod schema; DatePicker + TimePicker via
  Controller; price placeholder to USDC; removed alert() call
- signup-form.jsx: RHF + zod schema with password match refinement;
  inline per-field errors before submit; fixed ErrorMessage prop name
- login-form.jsx: RHF + zod schema; inline email/password validation
feat: migrate all forms to react-hook-form + zod validation
[Bug] PaymentModal abandons pending Stellar transactions instead of calling the existing cancelPayment
[Bug] Centralize auth state in a context provider and fix cookie inconsistencies
feat(#127): drive video player from real course data
feat(auth): add Sign in with Stellar SEP-10 wallet login #101
fix(wallet): handle missing wallet, wrong network, trustline, balance, and user rejection
@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
dnb-frontend Error Error Jul 31, 2026 10:22am

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR centralizes environment configuration and authentication, adds educator and landing-page experiences, updates AI chat, search, course progress, forms, dashboard widgets, and Stellar wallet flows, and applies route, accessibility, dependency, and service-worker updates.

Changes

Platform configuration and public profiles

Layer / File(s) Summary
Centralized configuration
.env.example, README.md, lib/config/*, lib/actions/*, lib/utils/*
Environment values now use validated shared configuration across API, AI, Firebase, Jitsi, Cloudinary, and Stellar integrations.
Landing and educator profiles
app/(pages)/(landingPage)/*, app/(pages)/educators/*, components/organisms/educators/*, components/molecules/dashboard/cards/educators/*
The landing page adds an animated global-reach section. Public educator pages load profiles, content, followers, ratings, sharing, and follow actions.
Authentication and route integration
components/providers/*, hooks/useAuth.js, components/organisms/auth/*, app/verify-email/*, app/layout.js
Authentication state moves into AuthProvider. Login adds Stellar authentication. Signup now uses email verification.
Stellar wallet and payment flow
components/stellar/*, hooks/useStellarAuth.js, hooks/useStellarPayment.js, lib/stellar/*, app/account/wallet/*
Wallet detection, network validation, payment pre-checks, structured errors, installation guidance, and transaction-history states are updated.
AI chat and command search
app/dashboard/ai/*, components/organisms/dashboard/CommandPalette.jsx, hooks/useSearch.js, lib/search-fallback.js
AI chat now streams responses for authenticated users. Command search adds recent searches, grouped results, debouncing, and navigation.
Course playback and progress
app/dashboard/courses/*, components/atoms/dashboard/vid-player-box.jsx, components/molecules/dashboard/cards/courseCard.jsx, hooks/useCourseProgress.js
Course playback supports resume, reset, completion, persisted preferences, progress reporting, and progress-aware course cards.
Library filters and dashboard experience
app/dashboard/library/*, components/molecules/dashboard/LibraryToolbar.jsx, components/organisms/dashboard/*, components/atoms/dashboard/*, components/molecules/Modal.js
The library adds URL-synchronized filtering and pagination. Dashboard widgets load dynamic data with loading and error states. Shared navigation, modal, notification, and accessibility behavior is updated.
Validated content forms
components/organisms/create/*
Book, course, and space forms use React Hook Form, Zod validation, upload checks, and controlled submission states.
Runtime consistency updates
app/dashboard/*/[[]*[]]/page.jsx, hooks/useNotificationSSE.js, next.config.mjs, package.json, public/sw.js, lib/data.js
Dynamic route parameters, SSE reconnection, dependencies, image hosts, service-worker assets, and unused data are updated.

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

Possibly related issues

Possibly related PRs

πŸš₯ Pre-merge checks | βœ… 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.45% 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 accurately identifies the PaymentModal cleanup, which is a real part of the changeset, although the pull request also includes broader unrelated changes.
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 πŸ’‘ 2
πŸ“ Generate docstrings πŸ’‘
  • Create stacked PR
  • Commit on current branch
πŸ› οΈ Fix failing CI checks πŸ’‘
  • Fix failing CI checks
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/payment-modal-cleanup

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

πŸ”§ Biome (2.5.5)
components/organisms/create/course-create-form.jsx

File contains syntax errors that prevent linting: Line 185: expected ) but instead found onSubmit; Line 193: expected , but instead found .; Line 196: Expected an expression but instead found '>'.; Line 202: expected , but instead found .; Line 206: Expected an expression but instead found '>'.; Line 210: expected , but instead found .; Line 211: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '('.; Line 211: Expected a function body but instead found '=>'.; Line 212: expected , but instead found setForm; Line 212: Expected a parameter but instead found '('.; Line 212: expected , but instead found prev; Line 212: Expected a function body but instead found '=>'.; Line 212: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 214: unterminated regex literal; Line 215: Expected a semicolon or an implicit semicolon after a statement, but found none; Line 215: Expected a semicolon or an imp

... [truncated 1284 characters] ...

und .; Line 255: expected , but instead found <; Line 256: Expected a parenthesis '(' but instead found '<'.; Line 257: Expected a parenthesis '(' but instead found '<'.; Line 258: Expected a parenthesis '(' but instead found 'Uploading'.; Line 258: expected , but instead found video; Line 258: expected , but instead found .; Line 259: Expected an expression but instead found '<'.; Line 260: Expected a parenthesis '(' but instead found '<'.; Line 261: Expected a parenthesis '(' but instead found ')'.; Line 269: expected , but instead found ||; Line 272: expected , but instead found .; Line 273: expected , but instead found ?; Line 278: Expected a statement but instead found '}


)'.; Line 282: Expected a statement but instead found '}'.


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

@zeemscript
zeemscript merged commit e20a734 into main Jul 31, 2026
2 of 4 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: 7

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (28)
components/organisms/create/book-create-form.jsx-24-29 (1)

24-29: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Reject an empty price instead of coercing it to 0.

price starts as '' at Line 36. z.coerce.number() converts '' to 0, and min(0) accepts it. A contributor who leaves the price field empty publishes a free book without any validation message. The same pattern exists in course-create-form.jsx and space-create-form.jsx.

Guard the empty string before coercion.

πŸ›‘οΈ Proposed fix
 const bookSchema = z.object({
   title: z.string().min(1, 'Title is required').max(200, 'Title is too long'),
   description: z.string().min(1, 'Description is required').max(5000, 'Description is too long'),
   category: z.string().min(1, 'Category is required'),
-  price: z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high'),
+  price: z
+    .string()
+    .min(1, 'Price is required')
+    .pipe(z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high')),
 });

Also applies to: 36-36

πŸ€– 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/create/book-create-form.jsx` around lines 24 - 29,
Update the price validation in bookSchema so an empty string is rejected before
numeric coercion can convert it to 0, while preserving the existing numeric
range checks. Apply the same empty-value guard to the price schemas in
course-create-form.jsx and space-create-form.jsx.
components/organisms/create/book-create-form.jsx-64-79 (1)

64-79: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Include the upload state in the submit gate, and guard payload.book._id.

Two gaps exist in the submit path:

  1. thumbnailError, fileError, and a missing file do not block submission. Zod validates only the text fields. A user can submit with no book file, or right after an invalid selection cleared file.
  2. Line 71 reads payload.book._id inside setTimeout. The callback runs outside the try block, so a missing book throws an uncaught TypeError and no toast appears.
πŸ›‘οΈ Proposed fix
     const onSubmit = async (data) => {
+      if (!file || fileError || thumbnailError) {
+        if (!file) setFileError('Book file is required');
+        return;
+      }
       try {
         const payload = await createBook({ form: data, thumbnail, file });
         if (payload.success) {
           toast.success('Book created successfully');
           if (onBookCreated) onBookCreated();
-          setTimeout(() => {
-            router.push(`/dashboard/library/${payload.book._id}`);
-          }, 2000);
+          const bookId = payload.book?._id;
+          if (bookId) {
+            setTimeout(() => router.push(`/dashboard/library/${bookId}`), 2000);
+          }
         } else {

Also applies to: 113-123

πŸ€– 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/create/book-create-form.jsx` around lines 64 - 79,
Update onSubmit to block createBook when thumbnailError or fileError exists, or
when file is missing, while preserving the existing validation and error toasts.
In the delayed navigation callback, guard payload.book and payload.book._id
before constructing the route, and surface a user-facing error instead of
allowing a missing book response to throw outside the try block.
components/organisms/create/course-create-form.jsx-128-136 (1)

128-136: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Bare Controller breaks the shared form primitives in both forms. FormItem, FormLabel, and FormMessage read FormFieldContext through useFormField, and only FormField provides that context (components/ui/form.jsx:13-23). Every field that wraps these primitives around a bare Controller loses its validation message. FormField forwards all props to Controller, so the render function needs no change.

  • components/organisms/create/course-create-form.jsx#L128-L136: replace the Controller for category with FormField.
  • components/organisms/create/space-create-form.jsx#L118-L134: replace the Controller for eventDate and the Controller for eventTime with FormField.

After both changes, drop the now unused Controller import in each file.

πŸ€– 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/create/course-create-form.jsx` around lines 128 - 136,
Replace the bare Controller components with FormField components in both files
because FormField provides the FormFieldContext needed by FormItem, FormLabel,
and FormMessage to work properly. In
components/organisms/create/course-create-form.jsx at lines 128-136, replace the
Controller wrapping the category field with FormField. In
components/organisms/create/space-create-form.jsx at lines 118-134, replace the
Controller wrapping the eventDate field and the Controller wrapping the
eventTime field with FormField. Since FormField forwards all props to
Controller, the render functions require no changes. After both changes, remove
the now-unused Controller import from each file while keeping all other imports
intact.
components/organisms/create/space-create-form.jsx-57-84 (1)

57-84: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Block submission on thumbnailError, and guard result.space._id.

Two gaps exist here:

  1. thumbnailError never blocks submission. After an invalid selection, thumbnail is null and the space is created without an image, with no visible reason.
  2. Line 76 reads result.space._id inside setTimeout. That callback runs after the try block exits, so a missing space throws an uncaught TypeError.

The same pattern exists in book-create-form.jsx Lines 64-79.

πŸ›‘οΈ Proposed fix
     const onSubmit = async (data) => {
+      if (thumbnailError) return;
       const formData = new FormData();
@@
-        if (result.success) {
+        if (result?.success) {
           toast.success('Space created successfully');
           if (onSpaceCreated) onSpaceCreated();
-          setTimeout(() => {
-            router.push(`/dashboard/spaces/${result.space._id}`);
-          }, 2000);
+          const spaceId = result.space?._id;
+          if (spaceId) {
+            setTimeout(() => router.push(`/dashboard/spaces/${spaceId}`), 2000);
+          }
         } else {
-          toast.error(result.message || 'Unknown error');
+          toast.error(result?.message || 'Unknown error');
         }
πŸ€– 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/create/space-create-form.jsx` around lines 57 - 84,
Update the onSubmit handler in space-create-form.jsx to return early with an
error toast when thumbnailError is present, preventing creation after an invalid
thumbnail selection. Before scheduling the router.push callback, validate that
result.space and result.space._id exist; only navigate when the identifier is
available, avoiding an uncaught asynchronous error. Apply the same changes to
the corresponding onSubmit flow in book-create-form.jsx.
hooks/useStellarAuth.js-41-45 (1)

41-45: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Fix unreachable "feature not deployed" branch caused by 404 check ordering.

The file's own contract comment (lines 16-17) documents two different meanings for HTTP 404: "Unregistered wallets: HTTP 404/409" and "Feature not deployed: challenge returns 404/503". isUnregisteredWallet (lines 41-45) treats a bare 404 as "wallet not linked" without distinguishing it from a route-not-found response. Because the check at line 173 (isUnregisteredWallet(error)) runs before the check at line 180 (error?.response?.status === 503 || error?.response?.status === 404), every 404 error is always classified as "wallet not linked" first. The 404 branch of the condition at Line 180 can never execute.

This matters in practice: if the Stellar auth feature becomes unreachable mid-session (server rollback, or the stellarAuthAvailable probe has not resolved yet when the user clicks the button), the user sees "This wallet is not linked to a Deen Bridge account" instead of the correct "Sign in with Stellar is not available on the server yet" message. That is a misleading error for a use case the code explicitly tries to support.

Reorder or disambiguate the checks so the two 404 meanings do not collide. For example, drop bare 404 from isUnregisteredWallet and rely on the explicit registered: false payload plus 409 for "unregistered," reserving 404/503 exclusively for "not deployed":

πŸ› Proposed fix to remove the 404 ambiguity
 const isUnregisteredWallet = (error, data) => {
   if (data && data.registered === false) return true;
   const status = error?.response?.status;
-  return status === 404 || status === 409;
+  return status === 409;
 };

Also applies to: 167-186

πŸ€– 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 `@hooks/useStellarAuth.js` around lines 41 - 45, Disambiguate HTTP 404 handling
between isUnregisteredWallet and the not-deployed error branch in the
authentication flow. Update isUnregisteredWallet to use data.registered ===
false or status 409, excluding bare 404, so the later status check around the
Stellar sign-in error handling can classify 404/503 as β€œnot available on the
server yet.”
hooks/useStellarAuth.js-187-197 (1)

187-197: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Standardize the final error branch: don't toast and re-throw.

Every earlier error branch (modal-closed, unregistered wallet, service unavailable) returns null or a plain object after showing a toast. This final fallback branch shows a toast (line 195) and then re-throws the error (line 196). If the caller of loginWithStellar() does not wrap the call in its own try/catch, this produces an unhandled promise rejection. If the caller does catch it and shows its own error UI, the user sees a duplicate error message.

Make this branch consistent with the others: return null after the toast, unless a caller genuinely needs to distinguish "recoverable, no error" cases from "unexpected failure" through an exception. If that distinction is needed, document it in the JSDoc contract at the top of the file, since it is currently not called out alongside the other listed behaviors.

πŸ› Proposed fix for consistent error handling
       console.error("Stellar auth error:", error);
       toast.error(message);
-      throw error;
+      return 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 `@hooks/useStellarAuth.js` around lines 187 - 197, Update the final error
branch in loginWithStellar to return null after displaying the existing toast
instead of re-throwing the error, matching the modal-closed,
unregistered-wallet, and service-unavailable branches. Only preserve exception
propagation if the JSDoc contract explicitly documents that callers must
distinguish unexpected failures.
hooks/useNotificationSSE.js-129-131 (1)

129-131: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Move the connectSSERef assignment into an effect declared before the initialization effect.

Render-time mutation can leave connectSSERef.current pointing to a callback from an uncommitted render. That callback can capture an uncommitted token or reconnectAttempts. Keep the dependency removal at Line 268 only after this synchronization is in place.

πŸ€– 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 `@hooks/useNotificationSSE.js` around lines 129 - 131, Move the
connectSSERef.current assignment into a useEffect declared before the
initialization effect in useNotificationSSE, synchronizing it with the current
connectSSE callback only after commit. Remove the render-time mutation and
retain the existing dependency removal only after this effect-based
synchronization is established.
hooks/useNotificationSSE.js-113-117 (1)

113-117: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Synchronize the retry counter before reconnecting.

setReconnectAttempts schedules a state update. The immediate connectSSERef.current?.() call can use a callback that captured the previous reconnectAttempts value. Automatic retries can repeat a delay and perform one extra retry. Manual reconnects can also use the old counter instead of zero.

Store the counter in a ref and update it before reconnecting, or start the connection after the state update commits. Reset the state and ref together after a successful connection and during manual reconnects.

πŸ€– 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 `@hooks/useNotificationSSE.js` around lines 113 - 117, Synchronize retry
tracking in the reconnect flow around connectSSERef.current by adding and
maintaining a ref alongside reconnectAttempts, incrementing it before invoking
the reconnect callback, and using it when calculating retry limits and delays.
Reset both the state value and ref after successful connections and during
manual reconnects so automatic and manual retries always observe the current
counter.

Source: Linters/SAST tools

app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-57-83 (1)

57-83: 🎯 Functional Correctness | 🟠 Major | πŸ—οΈ Heavy lift

Resume does not seek, and the saved position is overwritten before the learner can resume.

Thank you for adding resume support β€” the UX idea is good, but two things break it in this wiring.

  1. handleResume only sets useResume. VidPlayerBox keeps the same playerKey, and the inner MediaPlayer key in components/atoms/dashboard/vid-player-box.jsx (Line 244) depends only on video, subtitles, and chapters. The player therefore is not remounted and receives no seek request, so effectiveStartTime has no effect after the first mount.
  2. The player mounts at time 0 while the resume banner is displayed. The first time-update events report positions near 0, and reportProgress in hooks/useCourseProgress.js writes them straight to localStorage and to the backend. The stored resume point is destroyed, and the banner disappears because positionSeconds > 30 becomes false.

Default to the resume position when one exists, and remount the player when the start time changes.

πŸ› Proposed fix
-  const [useResume, setUseResume] = useState(false);
+  // Default to resuming so the first time-update does not overwrite the
+  // stored position before the learner chooses.
+  const [useResume, setUseResume] = useState(true);
   const [playerKey, setPlayerKey] = useState(0);
 
   const effectiveStartTime = useResume && resumeTime ? resumeTime : 0;
@@
   const handleResume = () => {
     setUseResume(true);
+    setPlayerKey((k) => k + 1);
   };
πŸ€– 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/courses/`[courseId]/CourseDetailPageClient.jsx around lines 57
- 83, Update CourseDetailPageClient’s resume flow so an available resumeTime is
selected by default rather than waiting for handleResume, while preserving
start-over as an explicit reset to zero. Ensure the player remount key changes
whenever effectiveStartTime changes, including resume and start-over
transitions, so VidPlayerBox/MediaPlayer receives the correct seek position and
early time-update events cannot overwrite the saved progress before resuming.
components/atoms/dashboard/vid-player-box.jsx-252-252 (1)

252-252: πŸ—„οΈ Data Integrity & Integration | 🟠 Major | πŸ—οΈ Heavy lift

Seek to startTime instead of setting clipStartTime.

When useResume is true, startTime is the persisted resumeTime. clipStartTime creates a clipped timeline, so PlayerProgressTracker reports a relative currentTime and clipped duration. This stores incorrect source-timeline progress and applies the 90% completion threshold to the remaining clip. Remove clipStartTime={startTime || undefined} and seek player.currentTime to startTime after the media is ready. Keep clipStartTime for actual clips only.

πŸ€– 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/atoms/dashboard/vid-player-box.jsx` at line 252, Update the video
player initialization around the clipStartTime prop and PlayerProgressTracker so
resumeTime is not passed as clipStartTime. Remove the resume-driven
clipStartTime assignment, and after the media becomes ready seek
player.currentTime to startTime when useResume is enabled; retain clipStartTime
only for actual clips and preserve source-timeline progress and duration
reporting.
.env.example-24-30 (1)

24-30: πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Remove the hardcoded Firebase API key.

The Firebase API key is committed in both the environment template and runtime fallback. This prevents key rotation from taking effect when environment configuration is absent. Replace template values with placeholders. Remove the hardcoded fallbacks. Provide deployment values through environment configuration. Rotate the exposed key and restrict it to the required browser origins and Google APIs.

  • .env.example#L24-L30: replace the Firebase project values with non-production placeholders.
  • lib/config/env.js#L111-L130: remove the hardcoded Firebase fallback values and reject missing required Firebase configuration.

As per path instructions, β€œFlag hardcoded secrets, API keys, or wallet secret keys.”

πŸ€– 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 @.env.example around lines 24 - 30, Remove the committed Firebase values from
.env.example lines 24-30 and replace them with non-production placeholders.
Update lib/config/env.js lines 111-130 to eliminate all hardcoded Firebase
fallback values and reject startup when required Firebase configuration is
missing, so deployments must provide it through environment variables. Rotate
the exposed API key and restrict its usage to required browser origins and
Google APIs.

Sources: Path instructions, Linters/SAST tools

lib/config/env.js-75-90 (1)

75-90: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Reject missing production API endpoints.

apiUrl and aiApiUrl silently fall back to localhost in production. The API routes then call the local application container instead of the configured backend. Fail configuration validation when either value is absent in production. Keep localhost defaults only for development.

πŸ€– 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/config/env.js` around lines 75 - 90, Update the apiUrl and aiApiUrl
getters in config to throw or otherwise fail validation when their corresponding
environment variables are missing in production, instead of returning localhost
defaults. Preserve the existing localhost fallback and warning behavior for
development environments, using the project’s established production-environment
check.
app/(pages)/(landingPage)/GlobalReach.jsx-142-147 (1)

142-147: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Use poppins_600.className inside cn().

poppins_600 is a next/font loader object, not a string. cn() uses clsx, which treats a plain object as a conditional class map. It therefore emits the object keys (className, style) as CSS class names and drops the real generated font class. The heading loses the Poppins 600 font, and invalid class names reach the DOM.

components/atoms/form/Button.jsx (line 34 in the provided snippet) already uses poppins_500.className, so follow that convention.

πŸ› Proposed fix
             <h2
               className={cn(
-                poppins_600,
+                poppins_600.className,
                 "text-4xl sm:text-5xl lg:text-7xl font-bold mt-4 bg-gradient-to-r from-green-400 via-green-500 to-green-600 text-transparent bg-clip-text"
               )}
             >
πŸ€– 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/`(pages)/(landingPage)/GlobalReach.jsx around lines 142 - 147, Update the
h2 className construction to pass poppins_600.className to cn() instead of the
next/font loader object, following the existing convention used by
poppins_500.className in Button.jsx.
app/(pages)/educators/[profileid]/EducatorPageClient.jsx-187-191 (1)

187-191: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

activeTab can point to a tab that does not exist, leaving the content area blank.

activeTab initializes to "courses" (line 45) and never resynchronizes. tabs drops every entry whose count is 0 (line 191). If the educator publishes books but no courses, then:

  • activeTab stays "courses".
  • The three render blocks at lines 245-265 all evaluate to false, so the page body is empty.
  • The tab bar renders only when tabs.length > 1 (line 219), so the user cannot select the books tab.

The educator page then appears broken for any educator with a single non-course content type.

πŸ› Proposed fix: derive the effective tab from the available tabs
   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);
+
+  // Fall back to the first available tab when the selected tab has no content.
+  const currentTab = tabs.some((t) => t.key === activeTab)
+    ? activeTab
+    : tabs[0]?.key;

Then replace activeTab with currentTab in the tab bar comparison (line 228) and in the three content conditions (lines 245, 252, 259).

πŸ€– 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/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 187 -
191, Derive a current/effective tab from the filtered tabs so the selected value
always matches an available tab, falling back to the first tab when activeTab is
absent. Use this currentTab value instead of activeTab in the tab-bar selection
comparison and all three content-render conditions, while preserving the
existing activeTab state and tab filtering.
app/(pages)/educators/[profileid]/EducatorPageClient.jsx-47-98 (1)

47-98: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Guard the state writes in the effect against out-of-order resolution.

The effect depends on profileid and currentUser?._id. currentUser resolves asynchronously from useAuth, so the effect commonly runs twice: once before authentication resolves and once after. Both runs call getUserById and the four settled fetches. Every setState call runs after an await, and no run is cancelled. The slower run therefore overwrites the newer data, and setLoading(false) from a stale run can end the loading state early.

Add a cancellation flag and check it before each write. This also removes the React Doctor no-set-state-after-await-in-effect error from CI.

πŸ›‘οΈ Proposed fix
   useEffect(() => {
+    let cancelled = false;
     async function load() {
       setLoading(true);
       setError(false);
       try {
         const res = await getUserById(profileid);
+        if (cancelled) return;
         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 (cancelled) return;
 

Add the same if (cancelled) return; check after the checkIfFollowing await, wrap the finally body in if (!cancelled), and return the cleanup:

     load();
+    return () => {
+      cancelled = true;
+    };
   }, [profileid, currentUser?._id]);
πŸ€– 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/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 47 -
98, Update the useEffect load flow in EducatorPageClient to use a per-run
cancelled flag, set it in the returned cleanup, and guard every state update
after an awaitβ€”including the user, courses, books, spaces, followers, and
following writes. Also guard the catch and finally state updates so stale runs
cannot set errors or loading, and check cancellation after checkIfFollowing
resolves.

Source: Linters/SAST tools

app/(pages)/(landingPage)/GlobalReach.jsx-3-3 (1)

3-3: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Declare cobe as a dependency. GlobalReach.jsx imports cobe, but the root package.json does not declare it. Clean installs and production builds can fail. ArabesqueDivider is exported correctly.

πŸ€– 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/`(pages)/(landingPage)/GlobalReach.jsx at line 3, Declare cobe as a
direct dependency in the root package manifest used by the project, ensuring the
locked dependency metadata is updated consistently. Keep the existing
createGlobe import in GlobalReach.jsx and the ArabesqueDivider export unchanged.
components/molecules/dashboard/cards/educators/PublicCourseCard.jsx-57-66 (1)

57-66: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Use public destinations for educator card links

app/dashboard/layout.jsx wraps these routes in ProtectedRoute, which redirects unauthenticated visitors to /login. No public course, book, or space detail routes exist.

Update all three links to public detail routes:

  • PublicCourseCard.jsx:62
  • PublicBookCard.jsx:51
  • PublicSpaceCard.jsx:87

If the dashboard routes are intentional, preserve the destination in ProtectedRoute; adding next to these links alone is insufficient because the guard redirects to /login without it.

πŸ€– 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/molecules/dashboard/cards/educators/PublicCourseCard.jsx` around
lines 57 - 66, Update the detail links in PublicCourseCard.jsx (57-66),
PublicBookCard.jsx (49-58), and PublicSpaceCard.jsx (83-91) to use the
corresponding public course, book, and space destinations instead of protected
dashboard routes. Do not add only a next parameter; ensure all three cards
navigate directly to publicly accessible detail routes.
components/organisms/dashboard/CommandPalette.jsx-248-265 (1)

248-265: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Render the reel and other result groups.

groupedResults builds reel and other buckets, but the JSX renders only course, book, user, and space. typeLinks.reel exists, so reels are searchable yet invisible. If a query returns only reels or an unknown type, results.length > 0 suppresses CommandEmpty at Line 306, and the palette shows just the "See all results" action. The user then sees an apparently empty result list.

Add the missing groups after the Spaces group.

πŸ› Proposed fix
             )}
+
+            {/* Reels */}
+            {groupedResults.reel.length > 0 && (
+              <CommandGroup heading="Reels">
+                {groupedResults.reel.map((item) => {
+                  const title = item.title || item.name || "Untitled Reel";
+                  return (
+                    <CommandItem
+                      key={`reel-${item.id}`}
+                      value={`reel-${item.id}-${title}`}
+                      onSelect={() =>
+                        handleSelect(typeLinks.reel(item.id), title)
+                      }
+                      className="cursor-pointer"
+                    >
+                      {getTypeIcon("reel")}
+                      <span className="truncate font-medium">{title}</span>
+                    </CommandItem>
+                  );
+                })}
+              </CommandGroup>
+            )}
           </>
         )}

Decide also how groupedResults.other should render, or drop the bucket if unknown types must be ignored.

Also applies to: 402-430

πŸ€– 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/CommandPalette.jsx` around lines 248 - 265,
Update the CommandPalette JSX rendering after the Spaces group to render
groupedResults.reel using typeLinks.reel, and add an explicit rendering decision
for groupedResults.other: either display unknown-type results through the
existing result-item path or remove the other bucket and ignore those results.
Ensure reel-only and unknown-type searches show visible results instead of only
the β€œSee all results” action.
app/dashboard/ai/page.jsx-42-58 (1)

42-58: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Add a cancellation flag to the chat-history effect.

The effect writes setChatId and setMessages after two awaits. If user?.id changes, or if the user selects a chat in the sidebar while the requests are in flight, the late resolution overwrites the current conversation. The sync effect at Lines 61-66 sets the same state, so the two effects can race and the user can see a different chat than the one selected.

Gate the setters behind an ignore flag.

πŸ›‘οΈ Proposed fix
   useEffect(() => {
+    let ignore = false;
     const load = async () => {
       if (!user?.id) return;
       try {
         const data = await loadChatHistory(user.id, true);
+        if (ignore) return;
         if (data.chats?.length > 0) {
           const latestChat = data.chats[0];
-          setChatId(latestChat.chat_id);
           const history = await loadChatMessages(latestChat.chat_id);
+          if (ignore) return;
+          setChatId(latestChat.chat_id);
           setMessages(history);
         }
       } catch (error) {
         console.error("Error loading user data:", error);
       }
     };
     load();
+    return () => {
+      ignore = true;
+    };
   }, [user?.id]);
πŸ€– 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/ai/page.jsx` around lines 42 - 58, Update the chat-history
useEffect around load so it tracks an ignore/cancellation flag, checks that flag
before setChatId and setMessages after the awaited requests, and marks the flag
during cleanup. Preserve the existing user?.id guard and ensure late results
cannot overwrite a chat selected by the sidebar or a newer effect run.

Source: Linters/SAST tools

components/atoms/dashboard/Notybell.jsx-85-89 (1)

85-89: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Fix invisible empty-state text.

Modal.js sets an explicit bg-white background on DialogPrimitive.Content. The empty-state text here uses text-white/70, so it renders nearly invisible white-on-white when notes.length === 0. Change the text color to a visible one, for example text-muted-foreground.

🎨 Proposed fix
                     {notes.length === 0 && (
-                        <p className="text-center text-sm text-white/70">
+                        <p className="text-center text-sm text-muted-foreground">
                             You’re all caught up!
                         </p>
                     )}
πŸ€– 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/atoms/dashboard/Notybell.jsx` around lines 85 - 89, Update the
empty-state paragraph rendered when notes.length === 0 in Notybell to use a
visible text color such as text-muted-foreground instead of text-white/70,
preserving the existing message and layout.
components/organisms/dashboard/StatsOverview.jsx-51-77 (1)

51-77: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Retry button does not retry anything.

useStats() only fetches once, driven by user?._id; it has no dependency on retryKey and exposes no refetch function. The retryKey state added here is applied only to the success-branch grid (key={retryKey} on line 82), which is not rendered while error is true. Clicking "Retry" therefore does nothing observable: error stays true and the same error UI keeps rendering. Give useStats a way to re-run its fetch (accept retryKey as an effect dependency, or return a refetch callback) so the button has an actual effect.

πŸ”§ Suggested direction (requires updating hooks/useStats.js as well)
-    const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error } = useStats();
+    const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error, refetch } = useStats();
                 <Button round className="bg-accent text-white text-sm" onClick={() => setRetryKey(k => k + 1)}>
+                <Button round className="bg-accent text-white text-sm" onClick={() => refetch()}>
πŸ€– 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/StatsOverview.jsx` around lines 51 - 77, Make
the retry control in StatsOverview actually re-run the stats request: update
useStats and its invocation from StatsOverview to accept retryKey as a
fetch-effect dependency, or expose and call a refetch callback. Ensure clicking
Retry causes the failed request to execute again and refreshes the loading/error
state instead of only changing the success-branch grid key.
components/organisms/auth/login-form.jsx-188-200 (1)

188-200: πŸ“ Maintainability & Code Quality | 🟠 Major | ⚑ Quick win

Keyboard users cannot reach the unavailability tooltip.

stellarButton is disabled when stellarAuthAvailable === false. The tooltip trigger is the wrapping <span> at line 191. A plain <span> is not focusable, and a disabled <button> is removed from the tab order.

The result: a keyboard or screen-reader user sees a dead button and never receives the explanation that the SEP-10 endpoint is not deployed.

Add tabIndex={0} to the span so focus reaches the trigger. Radix Tooltip opens on focus. Rendering the same message as visible helper text below the button is an even more robust option, because it does not depend on hover or focus at all.

β™Ώ Proposed fix
                 <TooltipTrigger asChild>
-                  <span className="w-full inline-flex">{stellarButton}</span>
+                  <span className="w-full inline-flex" tabIndex={0}>
+                    {stellarButton}
+                  </span>
                 </TooltipTrigger>
πŸ€– 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/auth/login-form.jsx` around lines 188 - 200, Add
tabIndex={0} to the span wrapping stellarButton in the stellarAuthAvailable ===
false branch so keyboard users can focus the TooltipTrigger and receive the
unavailable-message content, while preserving the existing disabled-button
behavior.
app/verify-email/page.jsx-21-38 (1)

21-38: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Encode the token and check the response status.

Three points on this fetch chain:

  1. Line 21 interpolates token straight into the URL path. If a token ever contains /, +, ?, or #, the request targets the wrong path. Standard base64 contains / and +. Wrap it in encodeURIComponent.
  2. The code never checks res.ok. If the API returns a 500 with an HTML error page, res.json() throws and the user sees the generic "An error occurred" message instead of the real reason. Read the status first.
  3. The fetch has no timeout. If the API hangs, the page stays on the spinner forever with no way out except a manual reload. AbortSignal.timeout gives the user a definite outcome.

The effect also has no cancellation guard, so a fast unmount leaves a state update targeting an unmounted component. The abort controller in the fix below covers that too.

πŸ”§ Proposed fix
-    fetch(`${config.apiUrl}/api/auth/verify-email/${token}`)
-      .then((res) => res.json())
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), 15000);
+
+    fetch(
+      `${config.apiUrl}/api/auth/verify-email/${encodeURIComponent(token)}`,
+      { signal: controller.signal }
+    )
+      .then(async (res) => {
+        const body = await res.json().catch(() => ({}));
+        if (!res.ok) {
+          throw new Error(body.message || `Verification failed (${res.status})`);
+        }
+        return body;
+      })
       .then((data) => {
         if (data.success) {
           setMessage(data.message || "Email verified successfully!");
           if (data.accessToken && data.user) {
             persistSession(data.accessToken, data.user);
           }
           setStatus("success");
         } else {
           setStatus("error");
           setMessage(data.message || "Verification failed.");
         }
       })
-      .catch(() => {
+      .catch((err) => {
+        if (err.name === "AbortError") return;
         setStatus("error");
-        setMessage("An error occurred. Please try again.");
-      });
+        setMessage(err.message || "An error occurred. Please try again.");
+      })
+      .finally(() => clearTimeout(timer));
+
+    return () => {
+      clearTimeout(timer);
+      controller.abort();
+    };
   }, [searchParams]);
πŸ€– 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/verify-email/page.jsx` around lines 21 - 38, Update the verification
fetch in the effect to wrap token with encodeURIComponent, use an
AbortController timeout, and guard state updates after cancellation or unmount.
Check res.ok before parsing JSON, preserving the API error message when
available while handling aborted or other failures with the existing error state
and message.
app/verify-email/page.jsx-1-9 (1)

1-9: 🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

Wrap the useSearchParams consumer in a Suspense boundary.

Move the verification logic into an inner client component and render it inside <Suspense> with the existing loading UI as the fallback. Without this boundary, the /verify-email production build can fail during static prerendering.

πŸ€– 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/verify-email/page.jsx` around lines 1 - 9, Update VerifyEmailPage by
moving the useSearchParams-dependent verification logic into an inner client
component, then render that component inside a React Suspense boundary with the
existing loading UI as its fallback; keep routing, session persistence, and
verification behavior unchanged.

Source: Path instructions

hooks/useAuth.js-7-7 (1)

7-7: πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Apply one secure cookie policy to every auth-cookie write.

COOKIE_OPTIONS covers only persistSession. The refresh and refreshUser paths still write cookies without secure or sameSite. Reuse the shared options in lib/config/axios.config.js and components/providers/AuthProvider.jsx.

πŸ€– 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 `@hooks/useAuth.js` at line 7, Update COOKIE_OPTIONS in useAuth and reuse the
same shared cookie options for every auth-cookie write, including
persistSession, refresh, and refreshUser. Ensure the writes in axios.config.js
and AuthProvider use options that include secure and sameSite, rather than
maintaining separate incomplete option objects.
components/stellar/StellarProvider.jsx-49-89 (1)

49-89: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Wallet-extension detection only covers Freighter.

hasWalletExtension is set only from window.freighter.isConnected(), at Line 74. The kit is initialized with FreighterModule, xBullModule, and AlbedoModule at Lines 57-61. A user with only xBull or Albedo installed still gets hasWalletExtension = false. Downstream components use this flag to decide between "Install Wallet" and "Connect Wallet" UI. Those users see an incorrect "install a wallet" prompt even though a supported wallet is already installed.

Check whether the kit exposes a method to enumerate installed/supported wallets across all configured modules (the kit documentation references a method that "will let you know those the user have already installed/has access to"), and use it instead of checking window.freighter directly.

πŸ€– 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/stellar/StellarProvider.jsx` around lines 49 - 89, Update wallet
detection in the `detectWallets` effect to use StellarWalletsKit’s
installed/accessibile-wallet enumeration API across all configured modules
(`FreighterModule`, `xBullModule`, and `AlbedoModule`) instead of checking
`window.freighter.isConnected()`. Set `hasWalletExtension` true when any
supported wallet is available, and preserve the existing false fallback and
error handling when none are accessible or detection fails.
components/stellar/PaymentModal.jsx-100-131 (1)

100-131: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

User cancellation is shown as a hard failure.

executePayment now returns plain false when the user rejects signing (see hooks/useStellarPayment.js). At Line 108-115, handleConfirm treats any falsy return as a generic failure and calls setStep("error") with "Transaction failed. Please try again." A user who cancels the wallet prompt sees a scary error screen, even though the hook already shows an info toast for the same cancellation. The dedicated isUserRejection(err) handling in this function's own catch block, at Lines 121-125, is now unreachable, because executePayment no longer throws for rejections.

Fix the root cause in executePayment's return contract (see hooks/useStellarPayment.js) so cancellations can be distinguished from genuine failures here, and route cancellations back to "preview" instead of "error".

πŸ€– 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/stellar/PaymentModal.jsx` around lines 100 - 131, Update
executePayment in useStellarPayment.js so user signing cancellations preserve a
distinguishable rejection signal instead of returning plain false, allowing
PaymentModal’s handleConfirm to route them through isUserRejection and back to
the preview step. Keep genuine payment failures distinguishable so the existing
generic error handling remains unchanged.
components/stellar/StellarProvider.jsx-280-317 (1)

280-317: 🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

Use non-floating-point arithmetic for the balance check.

parseFloat(walletInfo.usdcBalance || 0) < (price || 0) at Line 306 compares USDC amounts using floating-point numbers. Floating-point representation can misround currency values at the boundary, which can incorrectly flag a sufficient balance as insufficient, or the reverse.

Convert both sides to a fixed integer unit (for example, cents) before comparing, or use a decimal-safe comparison helper.

πŸ’° Proposed fix using integer-cent comparison
+      const toCents = (v) => Math.round(parseFloat(v || 0) * 100);
       if (
         connectedWallet &&
         walletInfo &&
-        parseFloat(walletInfo.usdcBalance || 0) < (price || 0)
+        toCents(walletInfo.usdcBalance) < toCents(price)
       ) {

As per path instructions, "Flag any deviation from this flow, any secret-key handling, and any amount arithmetic done with floating point."

πŸ€– 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/stellar/StellarProvider.jsx` around lines 280 - 317, Replace the
floating-point balance comparison in validateForPayment with decimal-safe
fixed-unit integer arithmetic or the existing decimal comparison helper,
converting both walletInfo.usdcBalance and price consistently before comparing.
Preserve the current insufficient_balance issue and message behavior, while
ensuring currency boundary values are compared exactly.

Source: Path instructions


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

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 7595c4e2-b595-4921-b632-ca4df5b6b1d4

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 606fd0e and 7822061.

β›” Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
πŸ“’ Files selected for processing (84)
  • .env.example
  • README.md
  • app/(pages)/(landingPage)/GlobalReach.jsx
  • app/(pages)/educators/[profileid]/EducatorPageClient.jsx
  • app/(pages)/educators/[profileid]/page.jsx
  • app/account/profile/[profileid]/page.jsx
  • app/account/wallet/page.jsx
  • app/api/ai/chat/route.js
  • app/api/ai/stream/route.js
  • app/api/books/[bookId]/preview/route.js
  • app/dashboard/ai/layout.js
  • app/dashboard/ai/page.jsx
  • app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx
  • app/dashboard/courses/[courseId]/page.jsx
  • app/dashboard/courses/page.jsx
  • app/dashboard/layout.jsx
  • app/dashboard/library/[bookid]/page.jsx
  • app/dashboard/library/page.jsx
  • app/dashboard/library/read/[bookid]/page.jsx
  • app/dashboard/search/[searchparam]/page.jsx
  • app/dashboard/spaces/[spacesid]/page.jsx
  • app/layout.js
  • app/verify-email/page.jsx
  • components/DebugAuthLogs.jsx
  • components/atoms/dashboard/Notybell.jsx
  • components/atoms/dashboard/Searchbox.jsx
  • components/atoms/dashboard/vid-player-box.jsx
  • components/atoms/form/ComboBox.jsx
  • components/atoms/reels/ReelActionButton.jsx
  • components/molecules/Modal.js
  • components/molecules/dashboard/LibraryToolbar.jsx
  • components/molecules/dashboard/Notybell.jsx
  • components/molecules/dashboard/cards/courseCard.jsx
  • components/molecules/dashboard/cards/educators/PublicBookCard.jsx
  • components/molecules/dashboard/cards/educators/PublicCourseCard.jsx
  • components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx
  • components/molecules/dashboard/cards/libraryCard.jsx
  • components/molecules/dashboard/cards/spaceCard.jsx
  • components/molecules/dashboard/nav-header.jsx
  • components/molecules/errors/NotFound.jsx
  • components/molecules/ladingpage/AnimatedSection.jsx
  • components/molecules/ladingpage/IslamicPattern.jsx
  • components/organisms/auth/login-form.jsx
  • components/organisms/auth/signup-form.jsx
  • components/organisms/create/book-create-form.jsx
  • components/organisms/create/course-create-form.jsx
  • components/organisms/create/space-create-form.jsx
  • components/organisms/dashboard/CommandPalette.jsx
  • components/organisms/dashboard/JaasMeetingClientSection.jsx
  • components/organisms/dashboard/LearningProgress.jsx
  • components/organisms/dashboard/StatsOverview.jsx
  • components/organisms/dashboard/UpcomingSessions.jsx
  • components/organisms/dashboard/ai/Ai-Sidebar.jsx
  • components/organisms/educators/EducatorProfileHeader.jsx
  • components/organisms/reels/ReelCard.jsx
  • components/organisms/reels/ReelFeed.jsx
  • components/providers/AppProviders.jsx
  • components/providers/AuthProvider.jsx
  • components/stellar/PaymentModal.jsx
  • components/stellar/StellarProvider.jsx
  • components/stellar/TransactionHistory.jsx
  • components/stellar/WalletConnectButton.jsx
  • components/stellar/WalletOnboarding.jsx
  • components/ui/command.jsx
  • hooks/useAuth.js
  • hooks/useCourseProgress.js
  • hooks/useDebouncedValue.js
  • hooks/useNotificationSSE.js
  • hooks/useSearch.js
  • hooks/useStellarAuth.js
  • hooks/useStellarPayment.js
  • hooks/useWalletReadiness.js
  • lib/actions/ai/load-chat-history.js
  • lib/actions/cached-api.js
  • lib/config/axios.config.js
  • lib/config/env.js
  • lib/config/firebase.config.js
  • lib/data.js
  • lib/search-fallback.js
  • lib/stellar/stellarErrors.js
  • lib/utils/cloudinaryUpload.js
  • next.config.mjs
  • package.json
  • public/sw.js
πŸ’€ Files with no reviewable changes (6)
  • hooks/useWalletReadiness.js
  • components/organisms/reels/ReelFeed.jsx
  • components/DebugAuthLogs.jsx
  • components/stellar/WalletOnboarding.jsx
  • package.json
  • lib/data.js

Comment on lines +25 to +50
function PlayerProgressTracker({ onTimeUpdate, onEnded }) {
const player = useMediaPlayer();

useEffect(() => {
if (!player) return;

const timeSub = player.on('time-update', (e) => {
if (onTimeUpdate) {
onTimeUpdate(e.currentTime, e.duration);
}
});

const endedSub = player.on('ended', () => {
if (onEnded) {
onEnded();
}
});

return () => {
timeSub();
endedSub();
};
}, [player, onTimeUpdate, onEnded]);

return 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 | πŸ”΄ Critical | ⚑ Quick win

🧩 Analysis chain

🌐 Web query:

Vidstack player time-update event detail currentTime duration subscribe API

πŸ’‘ Result:

In Vidstack Player, you can track media time and duration updates using events or state subscription hooks [1][2]. Event Handling The player dispatches specific DOM events that you can listen to for changes in time and duration [3][4]. 1. time-update Event: Fired periodically (approximately every 250ms) as the media plays or when the user seeks [3][5]. The event's detail contains the current playback time [3]. - Listener: player.addEventListener('time-update', (event) => { console.log(event.detail.currentTime); }); [5] 2. time-change Event: Fired when the currentTime property changes; it provides the "real" playback time without accounting for clipping [3]. 3. duration-change Event: Fired when the media's duration is determined or updated [3][5]. State Subscription For more efficient state managementβ€”especially in frameworks like Reactβ€”Vidstack provides hooks and subscription methods that avoid manual event binding [1]. - useMediaState: Use this hook to subscribe to specific pieces of state, such as currentTime or duration [1][6]. Example: const currentTime = useMediaState('currentTime', player); [7] - subscribe: You can call.subscribe directly on the player instance to watch for state changes [1][8]. This method returns an unsubscribe function to prevent memory leaks [9][8]. Example: player.subscribe(({ currentTime, duration }) => { console.log(currentTime, duration); }); [8] API Properties - currentTime: A number representing the current playback time in seconds [4][10]. Setting this value will seek the media to that position [4][10]. - duration: A number representing the total length of the media in seconds [4]. If the length is indefinite (e.g., live streams), it returns +Infinity [4]. It is recommended to use hooks like useMediaState or the player's subscribe method for state management to ensure clean, reactive updates within your application [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- candidate file ---'
fd -i 'vid-player-box\.jsx$' . | while read -r f; do
  wc -l "$f"
  sed -n '1,110p' "$f"
done

printf '%s\n' '--- progress hook references ---'
fd -i 'useCourseProgress\.js$' . | while read -r f; do
  wc -l "$f"
  sed -n '130,190p' "$f"
done

printf '%s\n' '--- Vidstack dependency declarations ---'
rg -n --glob 'package.json' --glob 'package-lock.json' --glob 'yarn.lock' --glob 'pnpm-lock.yaml' \
  '`@vidstack/react`|vidstack' .

Repository: Deen-Bridge/dnb-frontend

Length of output: 5534


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- exact dependency version ---'
sed -n '6095,6135p' package-lock.json

printf '%s\n' '--- progress callback wiring ---'
rg -n -C 6 'reportProgress|onTimeUpdate|PlayerProgressTracker|startTime' \
  components/atoms/dashboard/vid-player-box.jsx hooks/useCourseProgress.js

printf '%s\n' '--- relevant player implementation ---'
sed -n '110,280p' components/atoms/dashboard/vid-player-box.jsx

Repository: Deen-Bridge/dnb-frontend

Length of output: 17721


🌐 Web query:

Vidstack v1.12.13 media events time-update event.detail currentTime duration player.subscribe API

πŸ’‘ Result:

In Vidstack (v1.12.13 and related versions), the media playback state is managed through a reactive system where events and state subscriptions serve different purposes [1][2]. Media Events: time-update The time-update event is fired periodically (approximately every 250ms) as media playback progresses [3]. It is intended for tracking the displayed time in the UI [4][5]. Event Detail: The MediaTimeUpdateEventDetail interface includes the following properties: - currentTime: The current playback time in seconds [4]. - played: A TimeRanges object representing the time intervals that have been played [4][6]. Note: For receiving the real playback time without accounting for clipping (often called provider time), the time-change event is preferred over time-update [4][5]. State Management: player.subscribe API For tracking specific state changesβ€”such as currentTime or durationβ€”it is generally recommended to use the subscribe API rather than listening to events, as events are primarily for actions [1]. You can subscribe to the player state to receive updates reactively: player.subscribe((state) => { const { currentTime, duration } = state; console.log(Time: ${currentTime}, Duration: ${duration}); }); Alternatively, you can access current values directly via the player state properties: const currentTime = player.state.currentTime; const duration = player.state.duration; Key considerations: - State Subscription vs. Events: Use subscribe for tracking data (state changes) to ensure consistency, and use events for detecting user actions (e.g., play, pause, seek) [1]. - Tab Visibility: The time-update event may stop firing when the browser tab is inactive because Vidstack uses requestAnimationFrame for precise timing, which browsers typically throttle in background tabs [7]. If you require constant updates regardless of tab visibility, subscribing to state via the internal reactive system is generally more robust, though updates may still be limited by browser throttling of animation frames [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()

callback = re.search(
    r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
    player_file,
    re.S,
)
assert callback, "time-update callback not found"
body = callback.group("body")
assert "e.currentTime" in body and "e.duration" in body
assert "e.detail.currentTime" not in body

# Model the documented Vidstack event shape and the hook's runtime guard.
event = {"detail": {"currentTime": 12.5, "played": object()}}
position = event.get("currentTime")
duration = event.get("duration")
assert position is None and duration is None
assert "typeof positionSeconds !== 'number'" in hook_file
assert "typeof durationSeconds !== 'number'" in hook_file

print("callback reads top-level event fields:", True)
print("documented event fields are under event.detail:", True)
print("both values fail the reportProgress number guard:", True)
PY

printf '%s\n' '--- all progress callback callers ---'
rg -n -C 5 'reportProgress|onTimeUpdate=|onEnded=' --glob '!package-lock.json' .

Repository: Deen-Bridge/dnb-frontend

Length of output: 251


🏁 Script executed:

#!/bin/bash
set +e

python3 - <<'PY'
from pathlib import Path
import re

player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()

callback = re.search(
    r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
    player_file,
    re.S,
)
checks = [
    ("time-update callback found", callback is not None),
    ("top-level currentTime read", bool(callback and "e.currentTime" in callback.group("body"))),
    ("top-level duration read", bool(callback and "e.duration" in callback.group("body"))),
    ("detail currentTime absent", bool(callback and "e.detail.currentTime" not in callback.group("body"))),
    ("position number guard present", "typeof positionSeconds !== 'number'" in hook_file),
    ("duration number guard present", "typeof durationSeconds !== 'number'" in hook_file),
]
for label, result in checks:
    print(f"{label}: {result}")
if callback:
    print("--- callback body ---")
    print(callback.group("body"))

print("--- all progress callback callers ---")
PY
rg -n -C 5 'reportProgress|onTimeUpdate=|onEnded=' --glob '!package-lock.json' .

Repository: Deen-Bridge/dnb-frontend

Length of output: 6652


🏁 Script executed:

#!/bin/bash
set -eu

python3 - <<'PY'
from pathlib import Path
import re

player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()

callback = re.search(
    r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
    player_file,
    re.S,
)
assert callback
body = callback.group("body")
assert "e.currentTime" in body and "e.duration" in body

# Vidstack's documented v1.12.13 event shape.
event = {"detail": {"currentTime": 12.5, "played": object()}}
position = event.get("currentTime")
duration = event.get("duration")
assert position is None and duration is None

# Match the actual hook guard, which uses double-quoted literals.
guard = re.search(
    r'typeof positionSeconds !== "number".*?typeof durationSeconds !== "number"',
    hook_file,
)
assert guard

print("current callback reads top-level fields:", True)
print("representative time-update event has no top-level values:", True)
print("both callback arguments are undefined:", True)
print("useCourseProgress rejects non-number arguments:", True)
PY

printf '%s\n' '--- completion fallback and progress state logic ---'
sed -n '1,125p' hooks/useCourseProgress.js
sed -n '155,235p' hooks/useCourseProgress.js

Repository: Deen-Bridge/dnb-frontend

Length of output: 6444


Subscribe to player state for progress values

The time-update event stores currentTime in event.detail and does not provide duration. The current reads pass undefined to reportProgress, so normal playback progress is never recorded. Use player.subscribe(({ currentTime, duration }) => ...) and report only when duration > 0.

πŸ€– 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/atoms/dashboard/vid-player-box.jsx` around lines 25 - 50, Update
PlayerProgressTracker to subscribe to player state with player.subscribe(({
currentTime, duration }) => ...), replacing the time-update handler; call
onTimeUpdate only when duration > 0 and pass the subscribed currentTime and
duration values, while preserving the ended subscription and cleanup.

import React from "react";
import { SidebarTrigger } from "@/components/ui/sidebar";
import Notybell from "@/components/atoms/dashboard/Notybell";
import Notybell from "@/components/molecules/dashboard/Notybell";

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 | πŸ”΄ Critical | ⚑ Quick win

Import path points at the unwired SSE bell, not the live one.

components/molecules/dashboard/Notybell.jsx documents itself as "NOT YET WIRED UP" and states the bell currently rendered in the nav is components/atoms/dashboard/Notybell.jsx. This line imports from @/components/molecules/dashboard/Notybell, the molecules (SSE-backed) version, contradicting both files' own comments. Confirm which Notybell NavHeader must render. If the atoms version is still the intended live component, restore the import to @/components/atoms/dashboard/Notybell.

πŸ€– 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/molecules/dashboard/nav-header.jsx` at line 3, Update NavHeader’s
Notybell import to use the live wired component from the atoms/dashboard
location, replacing the unwired molecules/dashboard import while leaving the
rendered component usage unchanged.

Comment on lines +54 to +62
const handleFileChange = (e) => {
const f = e.target.files[0];
if (f) {
const result = validateFile(f, { maxSize: 100 * 1024 * 1024 });
if (!result.valid) { setFileError(result.error); setFile(null); return; }
}
setFileError('');
setFile(f || 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 | πŸ”΄ Critical | ⚑ Quick win

Add allowedTypes for the book file. The default allowlist rejects documents.

validateFile defaults allowedTypes to ["image/*", "video/*"] (see lib/utils/cloudinaryUpload.js:128-159). The call at Line 57 passes only maxSize, so the default applies. A PDF or EPUB book file fails with "File type application/pdf is not allowed", and setFile(null) clears the selection. This blocks the primary upload path of the form.

Pass the document types that createBook accepts.

πŸ› Proposed fix
     const handleFileChange = (e) => {
       const f = e.target.files[0];
       if (f) {
-        const result = validateFile(f, { maxSize: 100 * 1024 * 1024 });
+        const result = validateFile(f, {
+          maxSize: 100 * 1024 * 1024,
+          allowedTypes: ['application/pdf', 'application/epub+zip'],
+        });
         if (!result.valid) { setFileError(result.error); setFile(null); return; }
       }
πŸ“ 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 handleFileChange = (e) => {
const f = e.target.files[0];
if (f) {
const result = validateFile(f, { maxSize: 100 * 1024 * 1024 });
if (!result.valid) { setFileError(result.error); setFile(null); return; }
}
setFileError('');
setFile(f || null);
};
const handleFileChange = (e) => {
const f = e.target.files[0];
if (f) {
const result = validateFile(f, {
maxSize: 100 * 1024 * 1024,
allowedTypes: ['application/pdf', 'application/epub+zip'],
});
if (!result.valid) { setFileError(result.error); setFile(null); return; }
}
setFileError('');
setFile(f || 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 `@components/organisms/create/book-create-form.jsx` around lines 54 - 62,
Update the validateFile call inside handleFileChange to pass an allowedTypes
option containing the document MIME types accepted by createBook, while
retaining the existing 100 MB maxSize limit and validation flow so supported
PDF/EPUB selections are not rejected.

Comment on lines +73 to 107
const onSubmit = async (data) => {
try {
setLoading(true);
let thumbnailUrl = null;
let videoUrl = null;

// Upload thumbnail to Cloudinary first
if (thumbnail) {
toast.info("Uploading thumbnail...");
thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail);
}

// Upload video to Cloudinary
if (video) {
toast.info("Uploading video... This may take a while.");
videoUrl = await videoUpload.uploadFile(video);
}

// Now create course with URLs
const data = await createCourse({
form,
thumbnailUrl,
videoUrl,
category: form.category,
category: data.category,
});

if (data && data.success) {
if (result && result.success) {
toast.success("Course created successfully!");
router.push(`/dashboard/courses/${data.course._id}`);
router.push(`/dashboard/courses/${result.course._id}`);
} else {
toast.error(data?.message || "Failed to create course.");
toast.error(result?.message || "Failed to create course.");
}
} catch (error) {
toast.error(
error?.message || "An error occurred while creating the course."
);
toast.error(error?.message || "An error occurred while creating the course.");
} finally {
setLoading(false);
thumbnailUpload.reset();
videoUpload.reset();
}
};

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 | πŸ”΄ Critical | ⚑ Quick win

onSubmit does not compile, and it sends the wrong payload.

This block has four defects:

  1. Line 88 declares const data in the same function scope as the data parameter (Line 73). That is a redeclaration and raises SyntaxError: Identifier 'data' has already been declared. The module never loads.
  2. Line 89 passes form (the useForm return object) to createCourse instead of the validated values.
  3. Line 92 reads data.category inside the initializer of const data, so it hits the temporal dead zone.
  4. Lines 95-99 read result, which no scope declares.
πŸ› Proposed fix
-      const data = await createCourse({
-        form,
-        thumbnailUrl,
-        videoUrl,
-        category: data.category,
-      });
-
-      if (result && result.success) {
-        toast.success("Course created successfully!");
-        router.push(`/dashboard/courses/${result.course._id}`);
-      } else {
-        toast.error(result?.message || "Failed to create course.");
-      }
+      const result = await createCourse({
+        form: data,
+        thumbnailUrl,
+        videoUrl,
+        category: data.category,
+      });
+
+      if (result?.success) {
+        toast.success("Course created successfully!");
+        router.push(`/dashboard/courses/${result.course._id}`);
+      } else {
+        toast.error(result?.message || "Failed to create course.");
+      }

Confirm the parameter name that createCourse expects for the form values, because book-create-form.jsx uses form: data.

πŸ“ 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 onSubmit = async (data) => {
try {
setLoading(true);
let thumbnailUrl = null;
let videoUrl = null;
// Upload thumbnail to Cloudinary first
if (thumbnail) {
toast.info("Uploading thumbnail...");
thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail);
}
// Upload video to Cloudinary
if (video) {
toast.info("Uploading video... This may take a while.");
videoUrl = await videoUpload.uploadFile(video);
}
// Now create course with URLs
const data = await createCourse({
form,
thumbnailUrl,
videoUrl,
category: form.category,
category: data.category,
});
if (data && data.success) {
if (result && result.success) {
toast.success("Course created successfully!");
router.push(`/dashboard/courses/${data.course._id}`);
router.push(`/dashboard/courses/${result.course._id}`);
} else {
toast.error(data?.message || "Failed to create course.");
toast.error(result?.message || "Failed to create course.");
}
} catch (error) {
toast.error(
error?.message || "An error occurred while creating the course."
);
toast.error(error?.message || "An error occurred while creating the course.");
} finally {
setLoading(false);
thumbnailUpload.reset();
videoUpload.reset();
}
};
const onSubmit = async (data) => {
try {
let thumbnailUrl = null;
let videoUrl = null;
if (thumbnail) {
toast.info("Uploading thumbnail...");
thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail);
}
if (video) {
toast.info("Uploading video... This may take a while.");
videoUrl = await videoUpload.uploadFile(video);
}
const result = await createCourse({
form: data,
thumbnailUrl,
videoUrl,
category: data.category,
});
if (result?.success) {
toast.success("Course created successfully!");
router.push(`/dashboard/courses/${result.course._id}`);
} else {
toast.error(result?.message || "Failed to create course.");
}
} catch (error) {
toast.error(error?.message || "An error occurred while creating the course.");
} finally {
thumbnailUpload.reset();
videoUpload.reset();
}
};
πŸ€– 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/create/course-create-form.jsx` around lines 73 - 107,
Update onSubmit to avoid redeclaring its data parameter: pass the validated data
values as the createCourse payload’s form field, following the
book-create-form.jsx convention, and derive category from those values. Store
the createCourse response in a separately named result variable, then use that
result consistently for success routing and error messaging.

Source: Linters/SAST tools

Comment on lines 184 to +217
<form
onSubmit={handleSubmit}
className="w-sm sm:w-lg mx-auto rounded-xl p-1 space-y-3"
>
<Label htmlFor="title">Course title</Label>
<Input
id="title"
name="title"
placeholder="Course Title"
value={form.title}
onChange={handleChange}
required
/>
<Label htmlFor="title">Course description</Label>
<Label htmlFor="description">Course description</Label>
<Textarea
id="description"
name="description"
placeholder="Book Description"
value={form.description}
onChange={handleChange}
required
className="w-full h-24 resize-none overflow-y-auto"
/>
<Label htmlFor="title">Course Category</Label>
<Label htmlFor="category">Course Category</Label>
<CategoryCombobox
id="category"
category={form.category}
setCategory={(value) =>
setForm((prev) => ({ ...prev, category: value }))
}
/>
<Label htmlFor="title">Course price</Label>
<Label htmlFor="price">Course price</Label>
<Input
id="price"

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 | πŸ”΄ Critical | ⚑ Quick win

Remove the legacy form block. It breaks the build.

The return at Line 111 now contains two sibling elements: <Form> (Lines 112-183) and this <form>. JSX does not allow adjacent root elements, so Biome reports parse errors from Line 185 onward. This block also references identifiers that the rewrite removed: handleSubmit, handleChange, setForm, loading, and form.title. Label no longer appears in the import list either.

Delete Lines 184-280 so only the new <Form> tree remains. Please also run the linter locally before the next push, so CI stays green.

🧰 Tools
πŸͺ› Biome (2.5.5)

[error] 185-185: expected ) but instead found onSubmit

(parse)


[error] 193-193: expected , but instead found .

(parse)


[error] 196-196: Expected an expression but instead found '>'.

(parse)


[error] 202-202: expected , but instead found .

(parse)


[error] 206-206: Expected an expression but instead found '>'.

(parse)


[error] 210-210: expected , but instead found .

(parse)


[error] 211-211: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '('.

(parse)


[error] 211-211: Expected a function body but instead found '=>'.

(parse)


[error] 212-212: expected , but instead found setForm

(parse)


[error] 212-212: Expected a parameter but instead found '('.

(parse)


[error] 212-212: expected , but instead found prev

(parse)


[error] 212-212: Expected a function body but instead found '=>'.

(parse)


[error] 212-212: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 214-214: unterminated regex literal

(parse)


[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)


[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none

(parse)

πŸ€– 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/create/course-create-form.jsx` around lines 184 - 217,
Remove the legacy native form block beginning at the second <form> after the new
<Form> tree and continuing through its closing content, leaving only the
rewritten <Form> return tree. This eliminates the adjacent JSX root and stale
references such as handleSubmit, handleChange, setForm, loading, form, and
Label; run the linter afterward to verify the file parses cleanly.

Source: Linters/SAST tools

Comment on lines 88 to 95
setUnreadCount((prev) => {
const deletedNotification = notifications.find(
const deletedNotification = prev.find(
(n) => n._id === data.notificationId
);
return deletedNotification && !deletedNotification.isRead
? prev - 1
? Math.max(0, prev - 1)
: prev;
});

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

Fix the deleted-notification unread-count update.

setUnreadCount passes the numeric unread count as prev. At Line 89, prev.find(...) calls a list method on that number. A notification_deleted event will fail while React processes this updater.

Read the deleted item from the notification list, then update the numeric count. Use one reducer or a synchronized notification-list ref if concurrent events must use the latest list.

πŸ€– 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 `@hooks/useNotificationSSE.js` around lines 88 - 95, Fix the
notification_deleted updater in setUnreadCount so it does not call find on the
numeric unread count. Read the deleted notification from the maintained
notification list, then decrement the count only when that item exists and is
unread, preserving the Math.max(0, ...) guard; use a reducer or synchronized
list ref to ensure concurrent events read the latest list.

Comment on lines 80 to +107
if (user?._id) {
await refreshUser(user._id);
}

// Refresh wallet balance
await refreshBalance();

setCurrentTransaction(null);
return res.data;
return { success: true, data: res.data };
} else {
throw new Error(res.data.message);
}
} catch (error) {
const message = error.response?.data?.message || error.message;

// Handle specific Stellar errors
if (message.includes("insufficient") || message.includes("underfunded")) {
toast.error("Insufficient USDC balance");
} else if (message.includes("op_no_trust") || message.includes("trustline")) {
toast.error(
"You need to add USDC trustline to your wallet first"
);
} else if (message.includes("rejected") || message.includes("cancelled")) {
toast.error("Transaction was cancelled");
if (isUserRejection(error) || error.code === "USER_REJECTED") {
toast.info("Transaction cancelled", {
description: "You declined the signing request. No changes were made.",
});
return false;
}

const mapped = mapStellarError(error);
if (mapped) {
toast.error(mapped.title, {
description: mapped.nextStep,
});
} else {
const message = error.response?.data?.message || error.message;
toast.error(`Payment failed: ${message}`);
}
return false;
return { success: false, cancelled: isCancelled };

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.

πŸ—„οΈ Data Integrity & Integration | πŸ”΄ Critical | ⚑ Quick win

isCancelled is undefined β€” this throws on every non-rejection payment failure.

At Line 107, return { success: false, cancelled: isCancelled }; references isCancelled, which is never declared in this function. Any payment failure that is not a user rejection (a Stellar operation error, a network failure, a backend rejection) reaches this line and throws a ReferenceError. That error escapes executePayment and propagates uncaught into PaymentModal.handleConfirm, which then shows the raw "isCancelled is not defined" message on the error screen instead of the intended mapped error.

This also exposes a second, related problem: the return shape of executePayment is inconsistent across its three exit paths:

  • Success (Line 86): { success: true, data: res.data }.
  • User rejection (Line 95): plain false.
  • Other failure (Line 107): a broken object that throws before it can be returned.

PaymentModal's success screen reads result?.transaction?.explorerUrl directly on the returned value, so the new { success: true, data } wrapper means that field is now at result.data.transaction.explorerUrl instead, and the "View on Stellar Explorer" link silently stops appearing after a successful payment.

Make the return contract consistent, and remove the undefined reference.

πŸ› Proposed fix for a consistent return contract
       } catch (error) {
         if (isUserRejection(error) || error.code === "USER_REJECTED") {
           toast.info("Transaction cancelled", {
             description: "You declined the signing request. No changes were made.",
           });
-          return false;
+          return { success: false, cancelled: true };
         }

         const mapped = mapStellarError(error);
         if (mapped) {
           toast.error(mapped.title, {
             description: mapped.nextStep,
           });
         } else {
           const message = error.response?.data?.message || error.message;
           toast.error(`Payment failed: ${message}`);
         }
-        return { success: false, cancelled: isCancelled };
+        return { success: false, cancelled: false };
πŸ€– 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 `@hooks/useStellarPayment.js` around lines 80 - 107, Update executePayment so
every exit path returns a consistent result object: represent user rejection and
other failures with success false, and replace the undefined isCancelled
reference with the appropriate cancellation value or omit it. Preserve mapped
error handling and ensure the success response exposes transaction data where
PaymentModal expects result.transaction.explorerUrl, rather than nesting it
under result.data.

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.

8 participants