Skip to content

Dev - #165

Merged
zeemscript merged 54 commits into
mainfrom
dev
Jul 31, 2026
Merged

Dev#165
zeemscript merged 54 commits into
mainfrom
dev

Conversation

@zeemscript

@zeemscript zeemscript commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Summary by CodeRabbit

  • New Features

    • Added public educator profiles with courses, books, spaces, ratings, followers, sharing, and follow controls.
    • Added command-palette search with recent searches, grouped results, quick actions, and keyboard shortcuts.
    • Added library search, filtering, sorting, and pagination.
    • Added course progress tracking with resume, completion, restart, and review actions.
    • Added wallet installation guidance and clearer payment/network error messages.
  • Improvements

    • Enhanced login, signup, creation forms, notifications, accessibility, and upcoming-session displays.
    • Simplified wallet setup instructions and improved transaction history retry states.
    • Expanded configuration documentation.

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.
…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 24 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
# Conflicts:
#	components/atoms/dashboard/Notybell.jsx
#	components/atoms/dashboard/Searchbox.jsx
#	components/molecules/Modal.js
#	components/molecules/dashboard/nav-header.jsx
#	components/organisms/dashboard/StatsOverview.jsx
#	components/organisms/dashboard/UpcomingSessions.jsx
@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 2:44am

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

This PR centralizes environment configuration and expands authentication, Stellar wallet handling, educator profiles, course progress, library search, command search, form validation, and dashboard data loading.

Changes

Centralized configuration

Layer / File(s) Summary
Environment and service configuration
.env.example, README.md, lib/config/*, lib/actions/*, app/api/*, lib/utils/cloudinaryUpload.js, package.json
Adds validated shared configuration and updates API, AI, Firebase, Jitsi, and Cloudinary consumers. Removes unused dependencies and stale data exports.

Authentication and Stellar flows

Layer / File(s) Summary
Authentication context and login flows
components/providers/AuthProvider.jsx, hooks/useAuth.js, hooks/useStellarAuth.js, app/layout.js, components/organisms/auth/*
Adds centralized session state, cookie persistence, Stellar authentication, and React Hook Form/Zod validation.
Wallet readiness and payment handling
components/stellar/*, lib/stellar/stellarErrors.js, app/account/wallet/page.jsx
Adds wallet installation, network, trustline, balance, transaction, and mapped error states. Removes the wallet onboarding flow.

Educator and course experiences

Layer / File(s) Summary
Public educator profiles
app/(pages)/educators/*, components/organisms/educators/*, components/molecules/dashboard/cards/educators/*, app/account/profile/[profileid]/page.jsx
Adds public educator pages, metadata, profile actions, statistics, and public content cards. Updates educator links.
Course progress and playback
hooks/useCourseProgress.js, components/atoms/dashboard/vid-player-box.jsx, app/dashboard/courses/*, components/molecules/dashboard/cards/courseCard.jsx
Adds persisted playback progress, resume and completion actions, dynamic player loading, player preferences, chapters, and progress indicators.

Library and dashboard interactions

Layer / File(s) Summary
Library filters and command palette
app/dashboard/library/page.jsx, components/molecules/dashboard/LibraryToolbar.jsx, components/organisms/dashboard/CommandPalette.jsx, hooks/useSearch.js, hooks/useDebouncedValue.js, components/atoms/dashboard/Searchbox.jsx
Adds URL-synchronized library filters, sorting, pagination, local search fallback, and global command-palette navigation.
Dashboard data and accessibility updates
components/organisms/dashboard/*, components/atoms/*, components/molecules/Modal.js, components/organisms/reels/*, components/stellar/TransactionHistory.jsx
Replaces static dashboard data with fetched states, adds loading and retry handling, updates dialog behavior, and improves accessible labels and pressed states.

Content creation forms

Layer / File(s) Summary
Validated creation forms
components/organisms/create/*, components/atoms/form/ComboBox.jsx
Migrates book, course, and space creation forms to shared React Hook Form/Zod controls with field, file, and submission validation.

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

Possibly related issues

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 2.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title "Dev" is too generic and does not identify the pull request's main changes. Replace "Dev" with a concise title that describes the primary changes, such as centralized configuration and new educator profiles.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
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 dev

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)
app/layout.js

File contains syntax errors that prevent linting: Line 76: Expected a JSX Expression, a Element, or a text but instead found '<<<<<<'.; Line 77: expected > but instead found <; Line 76: Expected corresponding JSX closing tag for 'HEAD'.; Line 73: Expected corresponding JSX closing tag for 'body'.; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 87: Unexpected token. Did you mean {'>'} or &gt;?; Line 91: Unexpected token. Did you mean {'}'} or &rbrace;?; Line 92: expected < but instead the file ends

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 '}'.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

app/layout.js

Parsing error: Unexpected token (76:1)


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

@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: 9

Note

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
components/stellar/TransactionHistory.jsx (1)

53-72: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a cancellation guard before applying asynchronous results.

When role or pagination.page changes, an older request can resolve later and overwrite the newer transaction list. Skip all state updates from stale requests.

🤖 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/TransactionHistory.jsx` around lines 53 - 72, Update
fetchTransactions in TransactionHistory to track whether its request is still
current and skip all state updates from stale requests, including transactions,
pagination, error, and loading state. Ensure the guard is invalidated when the
effect reruns or unmounts, while preserving updates from the latest request.
🟠 Major comments (22)
components/molecules/dashboard/cards/educators/PublicCourseCard.jsx-14-21 (1)

14-21: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Every new public card marks its image as priority. The educator page renders these cards in a grid, so all card images preload eagerly and compete with the true LCP element.

  • components/molecules/dashboard/cards/educators/PublicCourseCard.jsx#L14-L21: remove priority and add a sizes value that matches the 1/2/3-column grid.
  • components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx#L38-L45: remove priority and add the same sizes value.
🤖 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 14 - 21, Update the Image elements in PublicCourseCard and PublicSpaceCard
to remove priority and add the same responsive sizes value matching the educator
page’s 1/2/3-column grid. Apply the change in
components/molecules/dashboard/cards/educators/PublicCourseCard.jsx lines 14-21
and components/molecules/dashboard/cards/educators/PublicSpaceCard.jsx lines
38-45.
app/(pages)/educators/[profileid]/EducatorPageClient.jsx-187-191 (1)

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

Set activeTab to the first available tab, otherwise the content area renders empty.

activeTab initializes to "courses". If the educator publishes only books or only spaces, courses.length is 0, so tabs contains one entry and the tab bar stays hidden (Line 219). Every grid condition at Lines 245-265 then fails, and the visitor sees the header with an empty content area.

Derive the effective tab from the available tabs.

🐛 Proposed fix
   const tabs = [
     { key: "courses", label: "Courses", count: courses.length, icon: GraduationCap },
     { key: "books", label: "Books", count: books.length, icon: BookOpen },
     { key: "spaces", label: "Spaces", count: spaces.length, icon: Users },
   ].filter((t) => t.count > 0);
+
+  const currentTab = tabs.some((t) => t.key === activeTab)
+    ? activeTab
+    : tabs[0]?.key;

Then use currentTab in the tab styling and in the three grid conditions:

-          {activeTab === "courses" && courses.length > 0 && (
+          {currentTab === "courses" && courses.length > 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 `@app/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 187 -
191, Derive a currentTab from the filtered tabs, falling back to the existing
activeTab when it is available and otherwise selecting the first tab’s key.
Update the tab styling and all three content-grid conditions in
EducatorPageClient to use currentTab so educators without courses still display
their available books or spaces.
app/(pages)/educators/[profileid]/EducatorPageClient.jsx-47-98 (1)

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

Guard the state writes after await with a cancellation flag.

The effect depends on currentUser?._id. Authentication resolves asynchronously, so the dependency changes from undefined to an id and the effect re-runs. Two in-flight load() calls can then resolve out of order and write stale educator, course, or follow state. The static analysis hint reports the same pattern.

🐛 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);
@@
           ]);
+        if (cancelled) return;
@@
         if (currentUser?._id && currentUser._id !== profileid) {
           const followRes = await checkIfFollowing(profileid);
-          if (followRes?.success) {
+          if (!cancelled && followRes?.success) {
             setIsFollowing(followRes.isFollowing);
           }
         }
       } catch (e) {
-        setError(true);
+        if (!cancelled) setError(true);
       } finally {
-        setLoading(false);
+        if (!cancelled) setLoading(false);
       }
     }
     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 to track cancellation for each effect
invocation and guard every state update after an await, including educator,
courses, books, spaces, follower count, follow status, errors, and loading. Set
the cancellation flag in the effect cleanup so an earlier load cannot overwrite
state after profileid or currentUser?._id changes, while preserving the existing
loading and error behavior for the active invocation.

Source: Linters/SAST tools

components/organisms/educators/EducatorProfileHeader.jsx-59-65 (1)

59-65: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Validate API dates before calling format. If createdAt or eventDate is invalid, date-fns/format throws RangeError and can break the public render. Check parsed.getTime() with Number.isNaN; omit the joined line or use "TBD" for invalid dates. A shared formatDateSafe helper can cover both sites.

🤖 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/educators/EducatorProfileHeader.jsx` around lines 59 -
65, Validate parsed dates before calling date-fns format in both
EducatorProfileHeader and PublicSpaceCard. Add or reuse a shared formatDateSafe
helper that checks parsed.getTime() with Number.isNaN, then omit the joined-date
line for invalid createdAt values and display "TBD" for invalid eventDate values
as appropriate.
components/organisms/create/book-create-form.jsx-28-28 (1)

28-28: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

An empty price passes validation as 0 in all three create forms. Each schema uses z.coerce.number().min(0, ...) while the matching default value is the empty string. Number('') is 0, and 0 satisfies min(0), so a user can submit the form without entering a price and the item is created for free. Apply the same z.preprocess guard in each schema so an empty value fails validation with "Price is required".

  • components/organisms/create/book-create-form.jsx#L28: wrap the price schema in z.preprocess and map '', null, and undefined to undefined.
  • components/organisms/create/course-create-form.jsx#L31: apply the same z.preprocess guard to the price schema.
  • components/organisms/create/space-create-form.jsx#L30: apply the same z.preprocess guard to the price schema.
🤖 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` at line 28, Prevent empty
prices from being coerced to zero by wrapping each price schema in a
z.preprocess guard that maps empty strings, null, and undefined to undefined,
then validates the value as required with the existing range constraints and
“Price is required” message. Apply this consistently in
components/organisms/create/book-create-form.jsx:28,
components/organisms/create/course-create-form.jsx:31, and
components/organisms/create/space-create-form.jsx:30.
lib/config/env.js-75-90 (1)

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

Fail startup when production service URLs are absent.

Lines 75-90 return localhost URLs when configuration is missing. In production, server routes will call a local service and client-side AI requests will target the user’s machine. Require both URLs in production and retain these fallbacks only for local 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 require NEXT_PUBLIC_API_URL and NEXT_PUBLIC_AI_API_URL when
running in production, failing startup instead of returning localhost fallbacks;
preserve the existing warning and localhost defaults for local development.
lib/config/env.js-111-130 (1)

111-130: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Remove the tracked Firebase project configuration.

The shared root cause is embedding deployed Firebase values in repository files. Use deployment-managed environment variables and placeholder values only.

  • lib/config/env.js#L111-L130: remove the Firebase fallback literals, including the API key, and validate required configuration.
  • .env.example#L24-L30: replace deployed project values with non-production placeholders.

Firebase browser configuration can be public, but this repository policy still prohibits hardcoded API keys. As per path instructions, “**/*.{js,jsx}: 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 `@lib/config/env.js` around lines 111 - 130, Remove all deployed Firebase
fallback literals from the firebase configuration getter in
lib/config/env.js#L111-L130, including the API key, and require validation of
the corresponding environment variables instead. Replace the deployed Firebase
values in .env.example#L24-L30 with clearly non-production placeholders; both
sites require direct changes while preserving the existing Firebase
configuration shape.

Sources: Path instructions, Linters/SAST tools

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

7-7: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Session cookies are written without secure or sameSite in two places. Both files persist authentication data with { expires: 1, path: "/" } and no transport or cross-site restrictions. The token cookie can travel over plaintext HTTP and is sent on top-level cross-site navigations. Define the attributes once and use that constant everywhere.

  • hooks/useAuth.js#L7-L7: extend the shared constant to { expires: 1, path: "/", sameSite: "strict", secure: process.env.NODE_ENV === "production" }, and export it so other modules reuse it rather than restating the shape.
  • components/providers/AuthProvider.jsx#L99-L99: import the exported constant and pass it to Cookies.set("userInfo", ...) instead of the inline { expires: 1, path: "/" } literal, so the two writers cannot drift apart.
🤖 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 the shared COOKIE_OPTIONS constant in
hooks/useAuth.js at line 7 to include sameSite: "strict" and production-only
secure behavior, then export it. In components/providers/AuthProvider.jsx at
line 99, import and reuse COOKIE_OPTIONS for the userInfo cookie instead of the
inline options object, ensuring both writers share the same attributes.
components/providers/AuthProvider.jsx-103-106 (1)

103-106: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Log only the error message, not the full error object.

console.log on an Axios error serializes error.response, and the response body of /api/users/:id is the user record. That can print the email and other identifiers into the browser console. Log the message instead, and use console.error so the level matches the event.

Nice work centralizing the session state here, by the way. This is the right place for it.

🔒️ Proposed fix for the log statement
     } catch (error) {
-      console.log("Failed to refresh user:", error);
+      console.error("Failed to refresh user:", error?.message);
       toast.error("Failed to refresh user info");
     }
🤖 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/providers/AuthProvider.jsx` around lines 103 - 106, Update the
catch block in AuthProvider to log only the error message rather than the full
Axios error object, and use console.error instead of console.log. Preserve the
existing toast notification while preventing response data from being serialized
into the browser console.
lib/stellar/stellarErrors.js-151-179 (1)

151-179: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Tighten the error classification. Two predicates currently overlap and one swallows real bugs.

Three separate problems here, all from loose substring matching:

  1. error.code === -1 matches in both isNoWalletError and isUserRejection. In components/stellar/StellarProvider.jsx line 180 the isNoWalletError branch runs first, so a user who simply closes the wallet modal gets the toast "No Stellar wallet detected. Install Freighter to continue." That is the wrong message for a cancel, and the "silent on rejection" branch at line 192 becomes unreachable for code -1.
  2. msg.includes("undefined") and msg.includes("Cannot read") classify any TypeError as "no wallet". A genuine null-dereference bug in the payment path then surfaces as an install prompt. That hides defects from you and misdirects the user. The selectWallet bug I flagged in hooks/useStellarAuth.js is exactly this shape.
  3. The matches are case-sensitive. Freighter and Albedo capitalize messages such as "Rejected by user", so those miss.

Drop the generic TypeError patterns, give the modal-closed code to one predicate only, and normalize case once.

🐛 Proposed fix for the predicates
 export function isNoWalletError(error) {
   if (!error) return false;
-  const msg = typeof error === "string" ? error : error.message || "";
-  return (
-    msg.includes("no wallet") ||
-    msg.includes("no extension") ||
-    msg.includes("not installed") ||
-    msg.includes("undefined") ||
-    msg.includes("Cannot read") ||
-    error.code === -1
-  );
+  const msg = (typeof error === "string" ? error : error.message || "").toLowerCase();
+  return (
+    msg.includes("no wallet") ||
+    msg.includes("no extension") ||
+    msg.includes("not installed") ||
+    msg.includes("wallet not found")
+  );
 }
 
 /**
  * Check if a signing error means the user rejected/cancelled.
  */
 export function isUserRejection(error) {
   if (!error) return false;
-  const msg = typeof error === "string" ? error : error.message || "";
+  const msg = (typeof error === "string" ? error : error.message || "").toLowerCase();
   return (
     msg.includes("rejected") ||
-    msg.includes("cancelled") ||
+    msg.includes("cancel") ||
     msg.includes("declined") ||
     msg.includes("denied") ||
-    msg.includes("User declined") ||
     error.code === 4001 ||
     error.code === -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 `@lib/stellar/stellarErrors.js` around lines 151 - 179, Tighten isNoWalletError
and isUserRejection: remove the generic "undefined" and "Cannot read" substring
checks, assign error.code === -1 to only the predicate representing modal
cancellation/user rejection, and eliminate it from the other predicate.
Normalize the extracted message once before performing all text comparisons so
capitalized wallet messages such as "Rejected by user" are recognized.
components/stellar/StellarProvider.jsx-291-300 (1)

291-300: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not compare USDC amounts with parseFloat.

Line 294 compares a balance against a price using IEEE-754 doubles. Stellar amounts carry seven decimal places, and binary floats cannot represent most decimal fractions exactly. A wallet holding exactly the price can compare as short, which blocks a valid purchase, and the reverse can let a payment through that Horizon then rejects with op_underfunded. The message on line 298 also rounds the balance to two decimals, so a user with 10.0000001 USDC reads "only have $10.00" next to a $10 price and cannot tell what is wrong.

Compare in integer stroops, or use a decimal library. BigInt on the scaled string needs no new dependency.

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

🐛 Proposed fix using integer stroops
+// Stellar amounts have 7 decimal places. Compare as integers to avoid float error.
+const toStroops = (value) => {
+  const [whole = "0", frac = ""] = String(value ?? "0").split(".");
+  return BigInt(whole || "0") * 10_000_000n + BigInt(frac.padEnd(7, "0").slice(0, 7));
+};
+
       if (
         connectedWallet &&
         walletInfo &&
-        parseFloat(walletInfo.usdcBalance || 0) < (price || 0)
+        toStroops(walletInfo.usdcBalance || 0) < toStroops(price || 0)
       ) {
         issues.push({
           ...WALLET_ERRORS.insufficient_balance,
-          message: `You need $${price} USDC but only have $${parseFloat(walletInfo.usdcBalance || 0).toFixed(2)}.`,
+          message: `You need ${price} USDC but only have ${walletInfo.usdcBalance || 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/stellar/StellarProvider.jsx` around lines 291 - 300, Update the
insufficient-balance check in the wallet validation flow to avoid parseFloat and
compare USDC values using exact integer stroops, such as BigInt conversion from
scaled decimal strings. Reuse the exact stroop values for both comparison and
the user-facing message, preserving sufficient precision instead of rounding the
balance to two decimals. Do not introduce secret-key handling or any
floating-point amount arithmetic.

Source: Path instructions

components/stellar/StellarProvider.jsx-54-89 (1)

54-89: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Use @stellar/freighter-api.isConnected() for Freighter detection.

The static StellarWalletsKit API is valid in v2.4.0. However, window.freighter is not an object with an isConnected() method. Use the installed @stellar/freighter-api package instead, or detection can incorrectly report Freighter as unavailable.

🤖 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 54 - 89, Update the
Freighter detection inside detectWallets to import and call isConnected() from
`@stellar/freighter-api` instead of accessing window.freighter.isConnected().
Preserve the existing setHasWalletExtension handling for successful detection
and failures, while keeping StellarWalletsKit initialization unchanged.
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-75-83 (1)

75-83: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

handleResume does not remount the player, so the resume position may be ignored.

handleStartOver bumps playerKey, but handleResume only sets useResume. That changes effectiveStartTime from 0 to resumeTime and passes it into VidPlayerBox as startTime, which maps to clipStartTime on MediaPlayer. The MediaPlayer key in components/atoms/dashboard/vid-player-box.jsx line 244 depends only on video, subtitles, and chapters, so the player instance is reused. A player that has already loaded its source does not necessarily re-apply a changed clipStartTime, so the learner can press Resume and stay at 0.

Bump playerKey in handleResume as well, so both controls behave the same way:

🐛 Proposed fix
   const handleResume = () => {
     setUseResume(true);
+    setPlayerKey((k) => k + 1);
   };

See the separate comment on clipStartTime in components/atoms/dashboard/vid-player-box.jsx. A seek on can-play is the better mechanism for resume, and it makes this remount unnecessary.

Also applies to: 195-203

🤖 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 75
- 83, Update handleResume in CourseDetailPageClient to increment playerKey after
enabling resume, matching handleStartOver’s remount behavior. Keep
setUseResume(true) unchanged so the remounted player receives the resume start
time.
hooks/useCourseProgress.js-269-311 (1)

269-311: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

loading never becomes false when backendAvailable === false.

The guard at line 270 returns before the try, so the finally block at line 307 never runs. setLoading(false) and fetchedRef.current = true are skipped. The hook then returns loading: true forever for every learner in a session where the progress endpoint answered 404 or 405 once.

app/dashboard/courses/page.jsx only destructures progressMap, so the defect is currently invisible. Any consumer that gates rendering on loading will show a permanent spinner.

The catch path at line 306 also calls loadAllFromLocal() without checking cancelled, so it sets state after the effect is torn down.

🐛 Proposed fix
     async function fetchAll() {
       if (backendAvailable === false) {
-        loadAllFromLocal();
-        return;
+        if (!cancelled) {
+          loadAllFromLocal();
+          fetchedRef.current = true;
+          setLoading(false);
+        }
+        return;
       }
@@
       } catch (err) {
         if (err?.response?.status === 404 || err?.response?.status === 405) {
           backendAvailable = false;
         }
-        loadAllFromLocal();
+        if (!cancelled) loadAllFromLocal();
       } finally {
         fetchedRef.current = true;
         if (!cancelled) setLoading(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/useCourseProgress.js` around lines 269 - 311, Update fetchAll in
useCourseProgress so the backendAvailable === false local-loading path also
marks fetchedRef.current and clears loading before returning, while preserving
cancellation safety. In the catch path, guard loadAllFromLocal with cancelled so
teardown cannot update state, and ensure the existing finally behavior remains
applied to normal requests.
hooks/useCourseProgress.js-161-195 (1)

161-195: 🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Throttle the state update and the localStorage write.

reportProgress runs on every Vidstack time-update event, which fires several times per second. Each call does two expensive things:

  1. setProgress with a fresh object, so the whole course detail page re-renders while the video plays.
  2. JSON.stringify plus a synchronous localStorage.setItem, which blocks the main thread on the same cadence.

Only the backend write is throttled today. The visible progress bar needs whole-percent granularity, so most of these updates produce no user-visible change.

⚡ Proposed fix
+  const lastLocalWriteRef = useRef(0);
+
   const reportProgress = useCallback(
     (positionSeconds, durationSeconds) => {
@@
       currentDataRef.current = data;
 
-      setProgress({
-        percent: pct,
-        positionSeconds,
-        durationSeconds,
-        completed: done,
-      });
-
-      writeLocalProgress(userId, courseId, data);
-
       const now = Date.now();
+
+      setProgress((prev) =>
+        prev.percent === pct && prev.completed === done
+          ? prev
+          : { percent: pct, positionSeconds, durationSeconds, completed: done }
+      );
+
+      if (now - lastLocalWriteRef.current >= LOCAL_WRITE_MS) {
+        lastLocalWriteRef.current = now;
+        writeLocalProgress(userId, courseId, data);
+      }
+
       if (now - lastWriteRef.current >= THROTTLE_MS) {

Add a constant near line 7, for example const LOCAL_WRITE_MS = 3000;, and keep the existing flushNow call in the unmount cleanup so the final position is still persisted.

Note that setProgress with a bailout keeps positionSeconds slightly stale between percent changes. The time readout at CourseDetailPageClient.jsx line 209 then advances in steps. If you want a smooth readout, keep the position in a ref and update state on a fixed interval instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useCourseProgress.js` around lines 161 - 195, Throttle both the visible
state update and localStorage persistence in reportProgress: update setProgress
only when the calculated whole percent changes, and gate writeLocalProgress with
a separate LOCAL_WRITE_MS interval (for example, 3000ms). Track the latest
progress data in currentDataRef, and preserve the existing flushNow unmount
cleanup so the final position is persisted.
hooks/useCourseProgress.js-76-139 (1)

76-139: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Add a cancellation flag to this effect.

The effect depends on courseId, and fetchFromApi calls setProgress, setLoading, and the refs after await. If a learner navigates from course A to course B before the first request resolves, the stale response for course A writes into the state and refs of the mount for course B. The player then shows the wrong resume position, and completedRef can be wrongly set to true.

useAllCourseProgress at line 267 already uses this pattern, so applying it here keeps both hooks consistent.

🛡️ Proposed fix
   useEffect(() => {
     if (!userId || !courseId) {
       setLoading(false);
       return;
     }
 
+    let cancelled = false;
+
     const local = readLocalProgress(userId, courseId);
@@
     async function fetchFromApi() {
       if (backendAvailable === false) {
-        setLoading(false);
+        if (!cancelled) setLoading(false);
         return;
       }
       try {
         const res = await axiosInstance.get("/api/progress/courses");
+        if (cancelled) return;
         if (res.data?.success && Array.isArray(res.data.progress)) {
@@
       } finally {
-        setLoading(false);
-        apiCheckedRef.current = true;
+        if (!cancelled) {
+          setLoading(false);
+          apiCheckedRef.current = true;
+        }
       }
     }
 
     fetchFromApi();
+
+    return () => {
+      cancelled = true;
+    };
   }, [userId, courseId]);
🤖 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/useCourseProgress.js` around lines 76 - 139, Add a cancellation flag
inside the useEffect and set it when the effect cleanup runs. In fetchFromApi,
after the await and before applying the response, updating progress, loading
state, or refs, return early when the effect has been cancelled; also guard the
finally updates so stale requests cannot affect the current course. Keep the
existing useAllCourseProgress cancellation pattern consistent.

Source: Linters/SAST tools

app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-69-73 (1)

69-73: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

handleEnded cannot mark completion when durationSeconds is 0.

handleEnded reports progress.durationSeconds for both the position and the duration. If the value is still 0, the call becomes reportProgress(0, 0). isCompleted returns false for a zero duration, so the course is never marked complete. This happens for a short video that ends before the first throttled state update lands, and after handleStartOver, which resets durationSeconds to 0.

The player already knows the real duration. Forward it from the ended event instead of reading it from state.

🐛 Proposed fix

In components/atoms/dashboard/vid-player-box.jsx, pass the duration on ended:

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

Then use it here:

-  const handleEnded = useCallback(() => {
-    if (course?._id) {
-      reportProgress(progress.durationSeconds || 0, progress.durationSeconds || 0);
-    }
-  }, [reportProgress, progress.durationSeconds, course?._id]);
+  const handleEnded = useCallback(
+    (duration) => {
+      const total = duration || progress.durationSeconds || 0;
+      if (!course?._id || total <= 0) return;
+      reportProgress(total, total);
+    },
+    [reportProgress, progress.durationSeconds, course?._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/courses/`[courseId]/CourseDetailPageClient.jsx around lines 69
- 73, Update the video player’s ended-event handling in VidPlayerBox to pass the
player’s actual duration, then change CourseDetailPageClient’s handleEnded to
accept and forward that duration for both reportProgress arguments instead of
using progress.durationSeconds.
components/organisms/dashboard/UpcomingSessions.jsx-18-34 (1)

18-34: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Distinguish a fetch failure from an empty list.

The catch block sets sessions to [], which renders the same "No upcoming sessions" empty state as a real fetch failure. A user cannot tell a backend outage from having no sessions, and this hides the failure from anyone watching for errors.

🔧 Proposed fix to surface fetch failures separately
     const [sessions, setSessions] = useState([]);
     const [loading, setLoading] = useState(true);
+    const [error, setError] = useState(false);

     useEffect(() => {
         let mounted = true;
         const fetch = async () => {
             try {
                 const data = await getSpaces();
                 if (!mounted) return;
                 const upcoming = (data || []).filter(s => s.status === "upcoming");
                 setSessions(upcoming);
+                setError(false);
             } catch {
-                if (mounted) setSessions([]);
+                if (mounted) {
+                    setSessions([]);
+                    setError(true);
+                }
             } finally {
                 if (mounted) setLoading(false);
             }
         };
         fetch();
         return () => { mounted = false; };
     }, []);

Then render a distinct message when error is true, instead of falling through to the empty-state branch.

Also applies to: 51-55

🤖 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/UpcomingSessions.jsx` around lines 18 - 34,
Update the UpcomingSessions useEffect and render logic to track fetch failure
separately from the sessions list: set an error state in the catch path instead
of treating failure as an empty result, and render a distinct failure message
when that state is true before the “No upcoming sessions” branch. Preserve the
existing empty-state behavior for successful fetches returning no upcoming
sessions.
components/organisms/dashboard/StatsOverview.jsx-53-79 (1)

53-79: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

The Retry button does not retry anything.

setRetryKey only updates local state. That state is read by the key prop on the success-branch container at Line 82, which is unreachable while error stays true. useStats (hooks/useStats.js) only re-runs its fetch effect when user?._id changes; it never re-fetches in response to retryKey. After a real failure, the user is stuck on the error screen: clicking "Retry" changes nothing, and the widget never recovers without a full page reload.

Expose a way to re-trigger the fetch from useStats, and call it from the Retry button.

🔧 Proposed fix: wire Retry to an actual refetch
-                <Button round className="bg-accent text-white text-sm" onClick={() => setRetryKey(k => k + 1)}>
+                <Button round className="bg-accent text-white text-sm" onClick={() => { setRetryKey(k => k + 1); refetch(); }}>
                     Retry
                 </Button>
-    const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error } = useStats();
+    const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error, refetch } = useStats();

hooks/useStats.js needs to expose refetch (outside the reviewed range):

export default function useStats() {
  const { user } = useAuth();
  const [stats, setStats] = useState({ /* ... */ loading: true, error: null });

  const fetchStats = useCallback(async () => {
    if (!user?._id) return;
    try {
      const res = await axiosInstance.get(`/api/users/${user._id}/stats`);
      setStats({ ...res.data, loading: false, error: null });
    } catch (error) {
      setStats((prev) => ({ ...prev, loading: false, error: error.message || "Failed to fetch stats" }));
    }
  }, [user?._id]);

  useEffect(() => {
    setStats((prev) => ({ ...prev, loading: true }));
    fetchStats();
  }, [fetchStats]);

  return { ...stats, refetch: fetchStats };
}

Also applies to: 81-88

🤖 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 53 - 79,
Expose a refetch function from useStats by reusing its fetch routine, then
destructure it in StatsOverview and invoke it from the Retry button alongside
any existing retry-state update. Ensure the fetch routine remains reactive to
user?._id and the button triggers a new request while the error view is
displayed.
components/organisms/dashboard/UpcomingSessions.jsx-68-69 (1)

68-69: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Guard against unparseable eventDate values before formatting.

If s.eventDate is truthy but invalid, date-fns v3.6.0 format() throws RangeError: Invalid time value during render. Use isValid() before formatting, preserve the truthy check, and reuse the parsed Date.

🤖 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/UpcomingSessions.jsx` around lines 68 - 69,
Update the event date rendering around the two spans to parse s.eventDate once,
preserve the existing truthy check, and use date-fns isValid() to verify the
parsed Date before calling format(). Render empty strings for missing or invalid
dates, while reusing the validated Date for both date and time formats.
components/organisms/dashboard/CommandPalette.jsx-40-46 (1)

40-46: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

"reel" results never render in the palette.

typeLabels (Lines 40-46), typeLinks (Lines 48-54), and getTypeIcon (Lines 56-71) all define a "reel" type, and groupedResults (Lines 247-265) buckets it. But the rendered sections (Lines 310-430) only cover course, book, user, and space. If the backend returns "reel" results, results.length > 0 is true, so the "No results found" empty state (Line 306-308) never shows either — matching reel items simply vanish from the UI.

Add a "Reels" CommandGroup, mirroring the existing pattern, or better, replace the four near-identical blocks with one config-driven loop over [{ type: "course", heading: "Courses" }, ...] (also note typeLabels at Lines 40-46 is currently unused — the headings are hardcoded strings — so it could drive this loop directly).

Also applies to: 310-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 40 - 46,
Update the rendered command sections in the CommandPalette component to include
groupedResults.reel, adding a “Reels” CommandGroup that follows the existing
course, book, user, and space rendering pattern. Prefer reusing typeLabels or a
shared configuration to avoid duplicated blocks, while preserving existing
headings, links, icons, and item rendering for all result types.
components/organisms/dashboard/CommandPalette.jsx-86-89 (1)

86-89: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Route "Open Wallet" to /account/wallet.

/account/wallet is the dedicated Stellar wallet-management route. /dashboard/earnings is the earnings analytics route and is already used by "Earnings".

🤖 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 86 - 89,
Update the "Open Wallet" entry in quickActions to use the dedicated
/account/wallet route instead of /dashboard/earnings; leave the existing label
and icon unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 32c92ef8-8342-4d58-b714-bacc1db68802

📥 Commits

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

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (74)
  • .env.example
  • README.md
  • 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/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
  • 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/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/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/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
  • package.json
💤 Files with no reviewable changes (6)
  • components/organisms/reels/ReelFeed.jsx
  • hooks/useWalletReadiness.js
  • package.json
  • components/DebugAuthLogs.jsx
  • components/stellar/WalletOnboarding.jsx
  • lib/data.js

Comment thread app/layout.js
Comment on lines +76 to +87
<<<<<<< HEAD
<ThemeProvider>
<AppearanceProvider>
<CacheProvider>
<StellarProvider>{children}</StellarProvider>
<AuthProvider>
<StellarProvider>{children}</StellarProvider>
</AuthProvider>
</CacheProvider>
<Toaster position="top-right" />
</AppearanceProvider>
</ThemeProvider>
>>>>>>> origin/dev

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

Remove the unresolved merge conflict markers.

Lines 76 and 87 still contain <<<<<<< HEAD and >>>>>>> origin/dev. JSX cannot parse these tokens, so next build and npm run lint both fail. Biome reports parse errors at the same lines. The provider nesting itself is correct, since StellarProvider calls useAuth, so it must stay inside AuthProvider. Only the markers need to go.

🐛 Proposed fix to remove the conflict markers
-<<<<<<< HEAD
         <ThemeProvider>
           <AppearanceProvider>
             <CacheProvider>
               <AuthProvider>
                 <StellarProvider>{children}</StellarProvider>
               </AuthProvider>
             </CacheProvider>
             <Toaster position="top-right" />
           </AppearanceProvider>
         </ThemeProvider>
->>>>>>> origin/dev
📝 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
<<<<<<< HEAD
<ThemeProvider>
<AppearanceProvider>
<CacheProvider>
<StellarProvider>{children}</StellarProvider>
<AuthProvider>
<StellarProvider>{children}</StellarProvider>
</AuthProvider>
</CacheProvider>
<Toaster position="top-right" />
</AppearanceProvider>
</ThemeProvider>
>>>>>>> origin/dev
<ThemeProvider>
<AppearanceProvider>
<CacheProvider>
<AuthProvider>
<StellarProvider>{children}</StellarProvider>
</AuthProvider>
</CacheProvider>
<Toaster position="top-right" />
</AppearanceProvider>
</ThemeProvider>
🧰 Tools
🪛 Biome (2.5.5)

[error] 76-76: Expected a JSX Expression, a Element, or a text but instead found '<<<<<<'.

(parse)


[error] 77-77: expected > but instead found <

(parse)


[error] 76-76: Expected corresponding JSX closing tag for 'HEAD'.

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)


[error] 87-87: Unexpected token. Did you mean {'>'} or &gt;?

(parse)

🪛 GitHub Actions: CI / 0_Lint and Build.txt

[error] 76-76: ESLint parsing error: Unexpected token at line 76. The 'npm run lint' command failed with exit code 1.

🪛 GitHub Actions: CI / Lint and Build

[error] 76-76: ESLint parsing error: Unexpected token at line 76. The 'npm run lint' command failed with exit code 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/layout.js` around lines 76 - 87, Remove the unresolved merge conflict
markers surrounding the provider JSX in the layout component, while preserving
the existing ThemeProvider, AppearanceProvider, CacheProvider, AuthProvider,
StellarProvider, and Toaster structure. Keep StellarProvider nested inside
AuthProvider and do not alter the provider behavior.

Source: Linters/SAST tools

Comment on lines 243 to +256
<MediaPlayer
src={data?.video}
key={JSON.stringify({ video: data.video, subtitles: data.subtitles, chapters: data.chapters })}
src={data.video}
viewType='video'
streamType='on-demand'
logLevel='warn'
playsInline
title={data?.title}
poster={data?.thumbnail}
clipStartTime={startTime || undefined}
storage={playbackPreferencesStorage}
onError={() => setHasPlaybackError(true)}
tabIndex={0}
aria-label={data?.title ? `Video player: ${data.title}` : 'Video player'}

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

Resume is built on a clip window instead of a seek. The saved position flows from useCourseProgress into MediaPlayer as clipStartTime, which redefines the playable range rather than seeking. That single decision produces both the unreachable early section and the rebased currentTime/duration that corrupt the recorded percentage. It also forces the caller to remount the player to apply a new position.

  • components/atoms/dashboard/vid-player-box.jsx#L243-L256: remove clipStartTime, accept startTime in PlayerProgressTracker, and seek once on the can-play event with player.remoteControl.seek(startTime).
  • app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx#L75-L83: once the player seeks on can-play, keep handleResume as a plain setUseResume(true). Until then, bump playerKey there so the new startTime actually reaches a fresh player instance.
📍 Affects 2 files
  • components/atoms/dashboard/vid-player-box.jsx#L243-L256 (this comment)
  • app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx#L75-L83
🤖 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 243 - 256, The
resume flow must seek to the saved position instead of using clipStartTime. In
components/atoms/dashboard/vid-player-box.jsx lines 243-256, remove
clipStartTime, pass startTime into PlayerProgressTracker, and seek once on
can-play via player.remoteControl.seek(startTime). In
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx lines 75-83, keep
handleResume as a plain setUseResume(true) and increment playerKey there so the
updated startTime reaches a fresh player instance.

Comment on lines +150 to +167
<Controller control={form.control} name="role" render={({ field }) => (
<FormItem>
<FormLabel>Role</FormLabel>
<FormControl>
<Select value={field.value} onValueChange={field.onChange}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="student">Student</SelectItem>
<SelectItem value="tutor">Tutor</SelectItem>
<SelectItem value="admin">Admin</SelectItem>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)} />

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

Form primitives are used without FormFieldContext in four places. FormItem, FormLabel, FormControl, and FormMessage all call useFormField(), and only FormField provides FormFieldContext (components/ui/form.jsx Lines 13-23). A bare Controller, or a FormLabel placed outside any field, leaves that context empty. The result is a missing field name, no aria-invalid wiring, no label association, and no validation message, and useFormField() can throw.

  • components/organisms/auth/signup-form.jsx#L150-L167: change the role Controller to FormField.
  • components/organisms/create/book-create-form.jsx#L113-L123: replace both standalone FormLabel elements with Label and add htmlFor="book-thumbnail" and htmlFor="book-file".
  • components/organisms/create/course-create-form.jsx#L128-L136: change the category Controller to FormField, and replace the standalone FormLabel elements on Lines 146 and 157 with Label plus htmlFor.
  • components/organisms/create/space-create-form.jsx#L118-L134: change the eventDate and eventTime Controller elements to FormField, and replace the standalone FormLabel on Line 145 with Label htmlFor="space-thumbnail".
📍 Affects 4 files
  • components/organisms/auth/signup-form.jsx#L150-L167 (this comment)
  • components/organisms/create/book-create-form.jsx#L113-L123
  • components/organisms/create/course-create-form.jsx#L128-L136
  • components/organisms/create/space-create-form.jsx#L118-L134
🤖 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/signup-form.jsx` around lines 150 - 167, Wrap the
role Controller in signup-form.jsx (150-167) with FormField so the form
primitives receive FormFieldContext. Apply the same Controller-to-FormField
change to eventDate and eventTime in
components/organisms/create/space-create-form.jsx (118-134) and category in
components/organisms/create/course-create-form.jsx (128-136). In
components/organisms/create/book-create-form.jsx (113-123), replace standalone
FormLabel elements with Label using htmlFor="book-thumbnail" and
htmlFor="book-file"; in course-create-form.jsx (128-136), replace the standalone
labels at lines 146 and 157 with Label and matching htmlFor values; in
space-create-form.jsx (118-134), replace the standalone thumbnail label at line
145 with Label htmlFor="space-thumbnail".

Comment on lines 88 to 100
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.");
}

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

🏁 Script executed:

#!/bin/bash
# Inspect the createCourse signature and payload handling.
fd -t f 'create-course.js' | xargs -r cat -n

Repository: Deen-Bridge/dnb-frontend

Length of output: 1053


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- target form ---'
cat -n components/organisms/create/course-create-form.jsx | sed -n '1,150p'

printf '%s\n' '--- createCourse call sites ---'
rg -n -C 8 'createCourse\s*\(' --glob '*.{js,jsx,ts,tsx}' .

printf '%s\n' '--- createCourse implementation ---'
fd -t f 'create-course.js' | while IFS= read -r file; do
  printf '\n--- %s ---\n' "$file"
  cat -n "$file"
done

Repository: Deen-Bridge/dnb-frontend

Length of output: 11812


🏁 Script executed:

#!/bin/bash
set -eu

printf '%s\n' '--- course response producers ---'
rg -n -C 6 'createCourse|/api/courses|Course created|success.*course|course.*success' \
  --glob '*.{js,jsx,ts,tsx}' \
  --glob '!node_modules' .

printf '%s\n' '--- standalone JavaScript scope check ---'
node - <<'JS'
function broken(onCreate) {
  const data = { category: "submitted" };
  try {
    const data = onCreate({ category: data.category });
    return data;
  } catch (error) {
    return `${error.name}: ${error.message}`;
  }
}

function fixed(onCreate) {
  const data = { category: "submitted" };
  try {
    const result = onCreate({ form: data, category: data.category });
    return result;
  } catch (error) {
    return `${error.name}: ${error.message}`;
  }
}

console.log("broken:", broken((payload) => payload));
console.log("fixed:", fixed((payload) => ({ success: true, payload })));

try {
  console.log(result);
} catch (error) {
  console.log("undeclared result:", `${error.name}: ${error.message}`);
}
JS

Repository: Deen-Bridge/dnb-frontend

Length of output: 27365


Fix the onSubmit payload and result binding.

The local data declaration shadows the submitted data parameter. data.category then throws ReferenceError: Cannot access 'data' before initialization. result is also undeclared. Pass form: data because createCourse reads form.title, form.description, and form.price from submitted values. Store the return value as result.

🤖 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 88 - 100,
Update the onSubmit handler to avoid shadowing its submitted data parameter:
pass that parameter as form when calling createCourse, and bind the returned
value to result before checking success and navigating or displaying the error
toast.

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.

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

The legacy form block breaks the build. Delete it.

The old markup remains after </Form>. Two sibling JSX elements are returned without a wrapper, so the file does not parse; Biome reports expected ")" but instead found "onSubmit" at Line 185 and many follow-on parse errors. The block also references identifiers that no longer exist in this component: handleSubmit, handleChange, setForm, loading, Label, and form.title/form.description/form.category/form.price as plain state. CI cannot pass in this state.

🐛 Proposed fix: remove Lines 184-280
       </form>
     </Form>
-    <form
-      onSubmit={handleSubmit}
-      className="w-sm sm:w-lg mx-auto  rounded-xl p-1 space-y-3"
-    >
-      <Label htmlFor="title">Course title</Label>
-      ... (delete the whole legacy form through its closing </form>)
-    </form>
   );
 };
📝 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
<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"
🧰 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,
Delete the entire legacy form JSX block beginning with the extra <form
onSubmit={handleSubmit}> after the existing </Form> and ending at its closing
tag. Preserve the current Form implementation and remove all references from
this obsolete block, including handleSubmit, handleChange, setForm, loading,
Label, and the plain form state fields.

Source: Linters/SAST tools

Comment on lines 49 to +62
const { initializePayment, executePayment, isProcessing } =
useStellarPayment();

const [step, setStep] = useState("preview"); // preview | confirm | processing | success | error
const [step, setStep] = useState("preview");
const [paymentData, setPaymentData] = useState(null);
const [result, setResult] = useState(null);
const [error, setError] = useState(null);
const [errorDetail, setErrorDetail] = useState(null);
const [showQr, setShowQr] = useState(false);
const [showWizard, setShowWizard] = useState(false);
const [preCheckIssues, setPreCheckIssues] = useState([]);

// Reset state when modal opens/closes
useEffect(() => {
if (isOpen) {
closingRef.current = false;

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

Two identifiers are used but never declared. The modal throws on open and on close.

  1. Line 62 assigns closingRef.current, and lines 133-134 read and write it, but no useRef call creates closingRef. useRef is imported on line 2 and never called. The reset effect runs as soon as isOpen turns true, so opening the modal throws ReferenceError: closingRef is not defined and React unmounts the tree. The purchase flow never renders.

  2. Lines 137 and 151 call cancelPayment(), but line 49 destructures only initializePayment, executePayment, and isProcessing from useStellarPayment(). cancelPayment is undefined, so closing the modal with a pending paymentData throws TypeError: cancelPayment is not a function. The pending transaction is then never released on the backend.

Declare the ref and pull cancelPayment out of the hook. The hook already exports it.

🐛 Proposed fix
-  const { initializePayment, executePayment, isProcessing } =
+  const { initializePayment, executePayment, cancelPayment, isProcessing } =
     useStellarPayment();
 
   const [step, setStep] = useState("preview");
   const [paymentData, setPaymentData] = useState(null);
   const [result, setResult] = useState(null);
   const [error, setError] = useState(null);
   const [errorDetail, setErrorDetail] = useState(null);
   const [showQr, setShowQr] = useState(false);
   const [preCheckIssues, setPreCheckIssues] = useState([]);
+  const closingRef = useRef(false);

Also applies to: 132-154

🤖 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 49 - 62, Update
PaymentModal’s useStellarPayment destructuring to include cancelPayment, and
initialize the existing closingRef with useRef before the effects that access
it. Preserve the current reset and cancellation flows so opening the modal no
longer throws and pending payments are released when closing or cancelling.

Comment on lines +115 to +139
// Check network alignment when wallet connects
useEffect(() => {
if (!connectedWallet || !kitInitialized) return;

async function checkNetwork() {
try {
const { address } = await StellarWalletsKit.authModal().catch(() => ({}));
// If we can reach this, wallet is available.
// The kit was initialized with the correct network, so if auth succeeds
// the wallet should be on the right network.
setNetworkMismatch(false);
setWalletNetwork(NETWORK);
} catch (error) {
if (!isNoWalletError(error)) {
// Some other issue — could be network mismatch
const msg = error?.message || "";
if (msg.includes("network") || msg.includes("mismatch")) {
setNetworkMismatch(true);
}
}
}
}

checkNetwork();
}, [connectedWallet, kitInitialized]);

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

This effect opens a wallet modal on page load. Remove the authModal() call.

StellarWalletsKit.authModal() is an interactive prompt. This effect runs whenever connectedWallet becomes truthy, and connectedWallet is restored from /api/stellar/wallet/me on every page load for any user with a saved wallet. So a returning user lands on the dashboard and a wallet selection modal appears with no action from them. Users read that as a phishing attempt, and it also races the real connectWallet modal.

The effect also cannot detect what it claims to. The comment on lines 122-124 concedes that a successful auth only proves the wallet is reachable, so networkMismatch is set from substring matching on an error message that a passing call never produces. The destructured address on line 121 is unused.

Two further defects in the same block: .catch(() => ({})) on line 121 swallows the error, so the catch on line 127 is unreachable for anything authModal throws. And per React Doctor, setNetworkMismatch/setWalletNetwork run after await with no cancellation guard, so overlapping runs can write stale values.

Read the network from the wallet without prompting. If the kit exposes getNetwork(), compare its passphrase against EXPECTED_PASSPHRASE. Otherwise drop the effect and rely on the signing failure, which already reports a network mismatch through mapStellarError.

🐛 Proposed direction: compare the network without an interactive prompt
   // Check network alignment when wallet connects
   useEffect(() => {
     if (!connectedWallet || !kitInitialized) return;
+    let cancelled = false;
 
     async function checkNetwork() {
       try {
-        const { address } = await StellarWalletsKit.authModal().catch(() => ({}));
-        // If we can reach this, wallet is available.
-        // The kit was initialized with the correct network, so if auth succeeds
-        // the wallet should be on the right network.
-        setNetworkMismatch(false);
-        setWalletNetwork(NETWORK);
-      } catch (error) {
-        if (!isNoWalletError(error)) {
-          // Some other issue — could be network mismatch
-          const msg = error?.message || "";
-          if (msg.includes("network") || msg.includes("mismatch")) {
-            setNetworkMismatch(true);
-          }
-        }
+        // Non-interactive read. Adjust to the kit's actual accessor.
+        const { networkPassphrase } = await StellarWalletsKit.getNetwork();
+        if (cancelled) return;
+        setNetworkMismatch(networkPassphrase !== EXPECTED_PASSPHRASE);
+        setWalletNetwork(networkPassphrase === Networks.PUBLIC ? "mainnet" : "testnet");
+      } catch {
+        if (cancelled) return;
+        // Unknown network: do not block the user on a failed probe.
+        setNetworkMismatch(false);
       }
     }
 
     checkNetwork();
+    return () => {
+      cancelled = true;
+    };
   }, [connectedWallet, kitInitialized]);
🧰 Tools
🪛 ast-grep (0.45.0)

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

(setstate-same-var)

🪛 React Doctor (0.9.1)

[error] 116-116: This setter runs after await, so overlapping re-runs of the effect can resolve out of order and write stale state; gate it behind a cancellation/ignore flag or return a cleanup that cancels the work.

In a useEffect whose dependencies can change, guard any setter call that runs after an await behind a cancellation/ignore flag, or return a cleanup that cancels the async work.

(no-set-state-after-await-in-effect)

🤖 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 115 - 139, Replace the
authModal-based logic in the network-alignment effect with a non-interactive
network read using StellarWalletsKit.getNetwork(), comparing its passphrase with
EXPECTED_PASSPHRASE and updating network state only while the effect is still
active. Remove the unused address and swallowed-error handling; if getNetwork()
is unavailable, remove this effect and rely on signing failures handled by
mapStellarError.

Source: Linters/SAST tools

Comment on lines 196 to +201
toast.error(error.message || "Failed to connect wallet");
}
} finally {
setIsConnecting(false);
}
}, [kitInitialized, user, refreshUser]);
}, [kitInitialized, user, refreshUser, selectWallet]);

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

selectWallet is referenced in two files but defined in neither. StellarProvider never declares a selectWallet function and never puts one in its context value, yet both files depend on it. The provider crashes on render, and Sign in with Stellar cannot obtain an address. One definition fixes both sites.

  • components/stellar/StellarProvider.jsx#L196-L201: define a selectWallet callback that opens the wallet picker and returns the chosen address, then add it to the value object at lines 307-323. Until it exists, remove it from this dependency array, because evaluating the array throws ReferenceError: selectWallet is not defined and takes down every page under the provider.
  • hooks/useStellarAuth.js#L71-L72: after the provider exports selectWallet, this destructure resolves and the call at line 133 works. Add a guard that reports a clear message if the context value is missing, so a future regression does not surface as TypeError: selectWallet is not a function behind a generic toast.
📍 Affects 2 files
  • components/stellar/StellarProvider.jsx#L196-L201 (this comment)
  • hooks/useStellarAuth.js#L71-L72
🤖 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 196 - 201, Define a
selectWallet callback in StellarProvider that opens the wallet picker and
returns the selected address, include it in the provider context value, and
retain it in the connect callback dependencies once defined. In
hooks/useStellarAuth.js, guard against a missing context selectWallet and report
a clear error before invoking it so regressions do not become a generic toast.
Affected sites: components/stellar/StellarProvider.jsx lines 196-201 require the
callback and context export; hooks/useStellarAuth.js lines 71-72 require the
missing-function guard.

Comment on lines 90 to +107
} 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

executePayment returns inconsistent shapes, and the consumer tests the result for truthiness. The producer returns { success: true, data }, { success: false }, and bare false from different paths. The consumer treats any truthy value as success, and every object is truthy. A failed payment therefore renders the success screen and fires onSuccess, which marks the item as purchased. Settle on one shape at the producer and read the flag at the consumer.

  • hooks/useStellarPayment.js#L90-L107: always return { success: boolean, cancelled: boolean, data?, error? }. Replace the bare false on line 95, and replace the undefined isCancelled on line 107 with false.
  • components/stellar/PaymentModal.jsx#L105-L114: change if (success) to read the flag, for example const result = await executePayment(paymentData); if (result?.success) { ... }. Handle result.cancelled by returning to the preview step instead of showing the error screen, since the user chose to cancel.
📍 Affects 2 files
  • hooks/useStellarPayment.js#L90-L107 (this comment)
  • components/stellar/PaymentModal.jsx#L105-L114
🤖 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 90 - 107, The payment result
contract is inconsistent, causing failed payment objects to be treated as
successful. In hooks/useStellarPayment.js lines 90-107, update executePayment to
always return { success, cancelled, data?, error? }, replacing the bare false
rejection result and undefined isCancelled with explicit boolean values. In
components/stellar/PaymentModal.jsx lines 105-114, inspect result.success rather
than result truthiness, and return to the preview step when result.cancelled is
true instead of showing the error screen.

@zeemscript
zeemscript merged commit 7c60797 into main Jul 31, 2026
2 of 5 checks passed
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