Fix/payment modal cleanup - #166
Conversation
- course-create-form.jsx: fixed duplicated htmlFor=title on all four labels (description/category/price labels pointed at the title input instead of their own field); added matching id to Input/Textarea; fixed Label id=thumbail/id=file typos to htmlFor=thumbnail/file so the upload section labels actually associate. - ComboBox.jsx (CategoryCombobox): added an id prop forwarded to the trigger button so the course-category label can associate with it. - Searchbox.jsx: added a visually-hidden (sr-only) label wired via useId, giving the search input an accessible name beyond just a placeholder. - TransactionHistory.jsx: added an aria-label to the icon-only explorer link and aria-hidden to its icon. - ReelActionButton.jsx: added aria-label to the button and aria-hidden to the icon span, so icon-only reel actions have an accessible name. StarRate.jsx already had a proper aria-label/role=img for read-only ratings, so left untouched. Scope note: this covers the concrete labeling/accessible-name items from #134 pointers. Contrast audit, skip-to-content link, shell landmarks, and wiring an automated a11y check are separate, larger pieces of this issue not yet addressed here. npm run lint and npm run build both pass.
β¦progress bars - Add useCourseProgress hook with localStorage write-behind and throttled API writes - Add useAllCourseProgress for batched progress on course listing pages - Extend VidPlayerBox with startTime, onTimeUpdate, and onEnded props - Add resume/start-over UX on course detail page with progress indicator - Add progress bars and completion badges to CourseCard for owned courses - Graceful degradation: works via localStorage when backend endpoints unavailable - Completion marked at >=90% watched or on player ended event Closes #108
β¦tracking feat(course-progress): add resume playback, completion tracking, and progress bars
β¦ (CodeRabbit)
label holds the visible count (e.g. 42), not an action name, so
aria-label={label} announced a bare number with no context for
like/love/comments/share buttons - or nothing at all where label
was numeric 0. Added a separate accessibleLabel prop used for
aria-label, and set it to Like/Love/Comments/Share at all 8
ReelActionButton call sites (desktop + mobile layouts) in
ReelCard.jsx. label remains the visible count only.
β¦rding Revert 137 feat/wallet onboarding
β¦deRabbit)
- accessibleLabel now folds in the visible count (e.g. "Like, 42")
instead of just the action name, so screen readers announce both.
- Added a pressed prop to ReelActionButton that renders aria-pressed,
wired to pressed={viewerLiked}/{viewerLoved} on the like/love
instances only, so assistive tech can tell whether a reaction is
currently active. Not applied to comments/share since those are not
toggle buttons.
- Add /educators/[profileid] public route accessible without authentication - Add EducatorProfileHeader with avatar, bio, role, stats, follower count - Add PublicCourseCard, PublicBookCard, PublicSpaceCard (no auth dependencies) - Add tabbed navigation for courses, books, and spaces - Add share button with Web Share API and clipboard fallback - Add auth-aware follow: logged-out users redirect to /login?next=... - Add privacy guard: shows not-found for missing/private profiles - Rewire instructor links on courseCard, libraryCard, spaceCard to /educators/[id] - Add 'View public page' link on authenticated profile page - Add generateMetadata for SEO (title, description, OpenGraph) Closes #116
β¦, and user rejection - Add lib/stellar/stellarErrors.js: reusable error mapping utility - StellarProvider: wallet detection, network mismatch check, validateForPayment() - PaymentModal: no-wallet install prompts, network mismatch warning, trustline/balance checks - WalletConnectButton: install prompts when no extension, network mismatch badge - useStellarPayment: graceful user rejection handling
ο»Ώfix: a11y label associations and accessible names (#134)
β¦storefront feat(educators): add public educator storefront pages
- Replace hardcoded files.vidstack.io demo subtitles/thumbnails with real course captions, sourced only from data.subtitles/data.chapters - Add chapter markers (VTT or array) with graceful degrade when absent - Persist playback rate/volume/muted to localStorage across courses - Add clear error state for missing/broken video instead of blank player - Add discoverable keyboard-shortcuts help and a focusable, labelled player - Lazy-load the player component to keep Vidstack off the initial bundle Progress/resume tracking intentionally left to #108.
β¦ shortcuts and type grouping
β¦t in dashboard layout
β¦closing via shortcut
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
- 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
β¦g, and update search fallback data
β¦tralized AppProviders component
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughThis PR centralizes environment configuration and authentication, adds educator and landing-page experiences, updates AI chat, search, course progress, forms, dashboard widgets, and Stellar wallet flows, and applies route, accessibility, dependency, and service-worker updates. ChangesPlatform configuration and public profiles
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
π₯ Pre-merge checks | β 4 | β 1β Failed checks (1 warning)
β Passed checks (4 passed)
β¨ Finishing Touches π‘ 2π Generate docstrings π‘
π οΈ Fix failing CI checks π‘
π§ͺ Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. π§ Biome (2.5.5)components/organisms/create/course-create-form.jsxFile contains syntax errors that prevent linting: Line 185: expected ... [truncated 1284 characters] ... und Comment |
There was a problem hiding this comment.
Actionable comments posted: 7
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
π Major comments (28)
components/organisms/create/book-create-form.jsx-24-29 (1)
24-29: π― Functional Correctness | π Major | β‘ Quick winReject an empty price instead of coercing it to 0.
pricestarts as''at Line 36.z.coerce.number()converts''to0, andmin(0)accepts it. A contributor who leaves the price field empty publishes a free book without any validation message. The same pattern exists incourse-create-form.jsxandspace-create-form.jsx.Guard the empty string before coercion.
π‘οΈ Proposed fix
const bookSchema = z.object({ title: z.string().min(1, 'Title is required').max(200, 'Title is too long'), description: z.string().min(1, 'Description is required').max(5000, 'Description is too long'), category: z.string().min(1, 'Category is required'), - price: z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high'), + price: z + .string() + .min(1, 'Price is required') + .pipe(z.coerce.number().min(0, 'Price must be 0 or greater').max(100000, 'Price seems too high')), });Also applies to: 36-36
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/create/book-create-form.jsx` around lines 24 - 29, Update the price validation in bookSchema so an empty string is rejected before numeric coercion can convert it to 0, while preserving the existing numeric range checks. Apply the same empty-value guard to the price schemas in course-create-form.jsx and space-create-form.jsx.components/organisms/create/book-create-form.jsx-64-79 (1)
64-79: π©Ί Stability & Availability | π Major | β‘ Quick winInclude the upload state in the submit gate, and guard
payload.book._id.Two gaps exist in the submit path:
thumbnailError,fileError, and a missingfiledo not block submission. Zod validates only the text fields. A user can submit with no book file, or right after an invalid selection clearedfile.- Line 71 reads
payload.book._idinsidesetTimeout. The callback runs outside thetryblock, so a missingbookthrows an uncaughtTypeErrorand no toast appears.π‘οΈ Proposed fix
const onSubmit = async (data) => { + if (!file || fileError || thumbnailError) { + if (!file) setFileError('Book file is required'); + return; + } try { const payload = await createBook({ form: data, thumbnail, file }); if (payload.success) { toast.success('Book created successfully'); if (onBookCreated) onBookCreated(); - setTimeout(() => { - router.push(`/dashboard/library/${payload.book._id}`); - }, 2000); + const bookId = payload.book?._id; + if (bookId) { + setTimeout(() => router.push(`/dashboard/library/${bookId}`), 2000); + } } else {Also applies to: 113-123
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/create/book-create-form.jsx` around lines 64 - 79, Update onSubmit to block createBook when thumbnailError or fileError exists, or when file is missing, while preserving the existing validation and error toasts. In the delayed navigation callback, guard payload.book and payload.book._id before constructing the route, and surface a user-facing error instead of allowing a missing book response to throw outside the try block.components/organisms/create/course-create-form.jsx-128-136 (1)
128-136: π©Ί Stability & Availability | π Major | β‘ Quick winBare
Controllerbreaks the shared form primitives in both forms.FormItem,FormLabel, andFormMessagereadFormFieldContextthroughuseFormField, and onlyFormFieldprovides that context (components/ui/form.jsx:13-23). Every field that wraps these primitives around a bareControllerloses its validation message.FormFieldforwards all props toController, so the render function needs no change.
components/organisms/create/course-create-form.jsx#L128-L136: replace theControllerforcategorywithFormField.components/organisms/create/space-create-form.jsx#L118-L134: replace theControllerforeventDateand theControllerforeventTimewithFormField.After both changes, drop the now unused
Controllerimport in each file.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/create/course-create-form.jsx` around lines 128 - 136, Replace the bare Controller components with FormField components in both files because FormField provides the FormFieldContext needed by FormItem, FormLabel, and FormMessage to work properly. In components/organisms/create/course-create-form.jsx at lines 128-136, replace the Controller wrapping the category field with FormField. In components/organisms/create/space-create-form.jsx at lines 118-134, replace the Controller wrapping the eventDate field and the Controller wrapping the eventTime field with FormField. Since FormField forwards all props to Controller, the render functions require no changes. After both changes, remove the now-unused Controller import from each file while keeping all other imports intact.components/organisms/create/space-create-form.jsx-57-84 (1)
57-84: π©Ί Stability & Availability | π Major | β‘ Quick winBlock submission on
thumbnailError, and guardresult.space._id.Two gaps exist here:
thumbnailErrornever blocks submission. After an invalid selection,thumbnailisnulland the space is created without an image, with no visible reason.- Line 76 reads
result.space._idinsidesetTimeout. That callback runs after thetryblock exits, so a missingspacethrows an uncaughtTypeError.The same pattern exists in
book-create-form.jsxLines 64-79.π‘οΈ Proposed fix
const onSubmit = async (data) => { + if (thumbnailError) return; const formData = new FormData(); @@ - if (result.success) { + if (result?.success) { toast.success('Space created successfully'); if (onSpaceCreated) onSpaceCreated(); - setTimeout(() => { - router.push(`/dashboard/spaces/${result.space._id}`); - }, 2000); + const spaceId = result.space?._id; + if (spaceId) { + setTimeout(() => router.push(`/dashboard/spaces/${spaceId}`), 2000); + } } else { - toast.error(result.message || 'Unknown error'); + toast.error(result?.message || 'Unknown error'); }π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/create/space-create-form.jsx` around lines 57 - 84, Update the onSubmit handler in space-create-form.jsx to return early with an error toast when thumbnailError is present, preventing creation after an invalid thumbnail selection. Before scheduling the router.push callback, validate that result.space and result.space._id exist; only navigate when the identifier is available, avoiding an uncaught asynchronous error. Apply the same changes to the corresponding onSubmit flow in book-create-form.jsx.hooks/useStellarAuth.js-41-45 (1)
41-45: π― Functional Correctness | π Major | β‘ Quick winFix unreachable "feature not deployed" branch caused by 404 check ordering.
The file's own contract comment (lines 16-17) documents two different meanings for HTTP 404: "Unregistered wallets: HTTP 404/409" and "Feature not deployed: challenge returns 404/503".
isUnregisteredWallet(lines 41-45) treats a bare 404 as "wallet not linked" without distinguishing it from a route-not-found response. Because the check at line 173 (isUnregisteredWallet(error)) runs before the check at line 180 (error?.response?.status === 503 || error?.response?.status === 404), every 404 error is always classified as "wallet not linked" first. The 404 branch of the condition at Line 180 can never execute.This matters in practice: if the Stellar auth feature becomes unreachable mid-session (server rollback, or the
stellarAuthAvailableprobe has not resolved yet when the user clicks the button), the user sees "This wallet is not linked to a Deen Bridge account" instead of the correct "Sign in with Stellar is not available on the server yet" message. That is a misleading error for a use case the code explicitly tries to support.Reorder or disambiguate the checks so the two 404 meanings do not collide. For example, drop bare 404 from
isUnregisteredWalletand rely on the explicitregistered: falsepayload plus 409 for "unregistered," reserving 404/503 exclusively for "not deployed":π Proposed fix to remove the 404 ambiguity
const isUnregisteredWallet = (error, data) => { if (data && data.registered === false) return true; const status = error?.response?.status; - return status === 404 || status === 409; + return status === 409; };Also applies to: 167-186
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useStellarAuth.js` around lines 41 - 45, Disambiguate HTTP 404 handling between isUnregisteredWallet and the not-deployed error branch in the authentication flow. Update isUnregisteredWallet to use data.registered === false or status 409, excluding bare 404, so the later status check around the Stellar sign-in error handling can classify 404/503 as βnot available on the server yet.βhooks/useStellarAuth.js-187-197 (1)
187-197: π©Ί Stability & Availability | π Major | β‘ Quick winStandardize the final error branch: don't toast and re-throw.
Every earlier error branch (modal-closed, unregistered wallet, service unavailable) returns
nullor a plain object after showing a toast. This final fallback branch shows a toast (line 195) and then re-throws the error (line 196). If the caller ofloginWithStellar()does not wrap the call in its owntry/catch, this produces an unhandled promise rejection. If the caller does catch it and shows its own error UI, the user sees a duplicate error message.Make this branch consistent with the others: return
nullafter the toast, unless a caller genuinely needs to distinguish "recoverable, no error" cases from "unexpected failure" through an exception. If that distinction is needed, document it in the JSDoc contract at the top of the file, since it is currently not called out alongside the other listed behaviors.π Proposed fix for consistent error handling
console.error("Stellar auth error:", error); toast.error(message); - throw error; + return null;π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useStellarAuth.js` around lines 187 - 197, Update the final error branch in loginWithStellar to return null after displaying the existing toast instead of re-throwing the error, matching the modal-closed, unregistered-wallet, and service-unavailable branches. Only preserve exception propagation if the JSDoc contract explicitly documents that callers must distinguish unexpected failures.hooks/useNotificationSSE.js-129-131 (1)
129-131: π©Ί Stability & Availability | π Major | β‘ Quick winMove the
connectSSERefassignment into an effect declared before the initialization effect.Render-time mutation can leave
connectSSERef.currentpointing to a callback from an uncommitted render. That callback can capture an uncommittedtokenorreconnectAttempts. Keep the dependency removal at Line 268 only after this synchronization is in place.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useNotificationSSE.js` around lines 129 - 131, Move the connectSSERef.current assignment into a useEffect declared before the initialization effect in useNotificationSSE, synchronizing it with the current connectSSE callback only after commit. Remove the render-time mutation and retain the existing dependency removal only after this effect-based synchronization is established.hooks/useNotificationSSE.js-113-117 (1)
113-117: π©Ί Stability & Availability | π Major | β‘ Quick winSynchronize the retry counter before reconnecting.
setReconnectAttemptsschedules a state update. The immediateconnectSSERef.current?.()call can use a callback that captured the previousreconnectAttemptsvalue. Automatic retries can repeat a delay and perform one extra retry. Manual reconnects can also use the old counter instead of zero.Store the counter in a ref and update it before reconnecting, or start the connection after the state update commits. Reset the state and ref together after a successful connection and during manual reconnects.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useNotificationSSE.js` around lines 113 - 117, Synchronize retry tracking in the reconnect flow around connectSSERef.current by adding and maintaining a ref alongside reconnectAttempts, incrementing it before invoking the reconnect callback, and using it when calculating retry limits and delays. Reset both the state value and ref after successful connections and during manual reconnects so automatic and manual retries always observe the current counter.Source: Linters/SAST tools
app/dashboard/courses/[courseId]/CourseDetailPageClient.jsx-57-83 (1)
57-83: π― Functional Correctness | π Major | ποΈ Heavy liftResume does not seek, and the saved position is overwritten before the learner can resume.
Thank you for adding resume support β the UX idea is good, but two things break it in this wiring.
handleResumeonly setsuseResume.VidPlayerBoxkeeps the sameplayerKey, and the innerMediaPlayerkey incomponents/atoms/dashboard/vid-player-box.jsx(Line 244) depends only onvideo,subtitles, andchapters. The player therefore is not remounted and receives no seek request, soeffectiveStartTimehas no effect after the first mount.- The player mounts at time 0 while the resume banner is displayed. The first
time-updateevents report positions near 0, andreportProgressinhooks/useCourseProgress.jswrites them straight to localStorage and to the backend. The stored resume point is destroyed, and the banner disappears becausepositionSeconds > 30becomes false.Default to the resume position when one exists, and remount the player when the start time changes.
π Proposed fix
- const [useResume, setUseResume] = useState(false); + // Default to resuming so the first time-update does not overwrite the + // stored position before the learner chooses. + const [useResume, setUseResume] = useState(true); const [playerKey, setPlayerKey] = useState(0); const effectiveStartTime = useResume && resumeTime ? resumeTime : 0; @@ const handleResume = () => { setUseResume(true); + setPlayerKey((k) => k + 1); };π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/dashboard/courses/`[courseId]/CourseDetailPageClient.jsx around lines 57 - 83, Update CourseDetailPageClientβs resume flow so an available resumeTime is selected by default rather than waiting for handleResume, while preserving start-over as an explicit reset to zero. Ensure the player remount key changes whenever effectiveStartTime changes, including resume and start-over transitions, so VidPlayerBox/MediaPlayer receives the correct seek position and early time-update events cannot overwrite the saved progress before resuming.components/atoms/dashboard/vid-player-box.jsx-252-252 (1)
252-252: ποΈ Data Integrity & Integration | π Major | ποΈ Heavy liftSeek to
startTimeinstead of settingclipStartTime.When
useResumeis true,startTimeis the persistedresumeTime.clipStartTimecreates a clipped timeline, soPlayerProgressTrackerreports a relativecurrentTimeand clippedduration. This stores incorrect source-timeline progress and applies the 90% completion threshold to the remaining clip. RemoveclipStartTime={startTime || undefined}and seekplayer.currentTimetostartTimeafter the media is ready. KeepclipStartTimefor actual clips only.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/atoms/dashboard/vid-player-box.jsx` at line 252, Update the video player initialization around the clipStartTime prop and PlayerProgressTracker so resumeTime is not passed as clipStartTime. Remove the resume-driven clipStartTime assignment, and after the media becomes ready seek player.currentTime to startTime when useResume is enabled; retain clipStartTime only for actual clips and preserve source-timeline progress and duration reporting..env.example-24-30 (1)
24-30: π Security & Privacy | π Major | β‘ Quick winRemove the hardcoded Firebase API key.
The Firebase API key is committed in both the environment template and runtime fallback. This prevents key rotation from taking effect when environment configuration is absent. Replace template values with placeholders. Remove the hardcoded fallbacks. Provide deployment values through environment configuration. Rotate the exposed key and restrict it to the required browser origins and Google APIs.
.env.example#L24-L30: replace the Firebase project values with non-production placeholders.lib/config/env.js#L111-L130: remove the hardcoded Firebase fallback values and reject missing required Firebase configuration.As per path instructions, βFlag hardcoded secrets, API keys, or wallet secret keys.β
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In @.env.example around lines 24 - 30, Remove the committed Firebase values from .env.example lines 24-30 and replace them with non-production placeholders. Update lib/config/env.js lines 111-130 to eliminate all hardcoded Firebase fallback values and reject startup when required Firebase configuration is missing, so deployments must provide it through environment variables. Rotate the exposed API key and restrict its usage to required browser origins and Google APIs.Sources: Path instructions, Linters/SAST tools
lib/config/env.js-75-90 (1)
75-90: π©Ί Stability & Availability | π Major | β‘ Quick winReject missing production API endpoints.
apiUrlandaiApiUrlsilently fall back to localhost in production. The API routes then call the local application container instead of the configured backend. Fail configuration validation when either value is absent in production. Keep localhost defaults only for development.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/config/env.js` around lines 75 - 90, Update the apiUrl and aiApiUrl getters in config to throw or otherwise fail validation when their corresponding environment variables are missing in production, instead of returning localhost defaults. Preserve the existing localhost fallback and warning behavior for development environments, using the projectβs established production-environment check.app/(pages)/(landingPage)/GlobalReach.jsx-142-147 (1)
142-147: π― Functional Correctness | π Major | β‘ Quick winUse
poppins_600.classNameinsidecn().
poppins_600is anext/fontloader object, not a string.cn()usesclsx, which treats a plain object as a conditional class map. It therefore emits the object keys (className,style) as CSS class names and drops the real generated font class. The heading loses the Poppins 600 font, and invalid class names reach the DOM.
components/atoms/form/Button.jsx(line 34 in the provided snippet) already usespoppins_500.className, so follow that convention.π Proposed fix
<h2 className={cn( - poppins_600, + poppins_600.className, "text-4xl sm:text-5xl lg:text-7xl font-bold mt-4 bg-gradient-to-r from-green-400 via-green-500 to-green-600 text-transparent bg-clip-text" )} >π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(pages)/(landingPage)/GlobalReach.jsx around lines 142 - 147, Update the h2 className construction to pass poppins_600.className to cn() instead of the next/font loader object, following the existing convention used by poppins_500.className in Button.jsx.app/(pages)/educators/[profileid]/EducatorPageClient.jsx-187-191 (1)
187-191: π― Functional Correctness | π Major | β‘ Quick win
activeTabcan point to a tab that does not exist, leaving the content area blank.
activeTabinitializes to"courses"(line 45) and never resynchronizes.tabsdrops every entry whosecountis0(line 191). If the educator publishes books but no courses, then:
activeTabstays"courses".- The three render blocks at lines 245-265 all evaluate to
false, so the page body is empty.- The tab bar renders only when
tabs.length > 1(line 219), so the user cannot select the books tab.The educator page then appears broken for any educator with a single non-course content type.
π Proposed fix: derive the effective tab from the available tabs
const tabs = [ { key: "courses", label: "Courses", count: courses.length, icon: GraduationCap }, { key: "books", label: "Books", count: books.length, icon: BookOpen }, { key: "spaces", label: "Spaces", count: spaces.length, icon: Users }, ].filter((t) => t.count > 0); + + // Fall back to the first available tab when the selected tab has no content. + const currentTab = tabs.some((t) => t.key === activeTab) + ? activeTab + : tabs[0]?.key;Then replace
activeTabwithcurrentTabin the tab bar comparison (line 228) and in the three content conditions (lines 245, 252, 259).π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 187 - 191, Derive a current/effective tab from the filtered tabs so the selected value always matches an available tab, falling back to the first tab when activeTab is absent. Use this currentTab value instead of activeTab in the tab-bar selection comparison and all three content-render conditions, while preserving the existing activeTab state and tab filtering.app/(pages)/educators/[profileid]/EducatorPageClient.jsx-47-98 (1)
47-98: π©Ί Stability & Availability | π Major | β‘ Quick winGuard the state writes in the effect against out-of-order resolution.
The effect depends on
profileidandcurrentUser?._id.currentUserresolves asynchronously fromuseAuth, so the effect commonly runs twice: once before authentication resolves and once after. Both runs callgetUserByIdand the four settled fetches. EverysetStatecall runs after anawait, and no run is cancelled. The slower run therefore overwrites the newer data, andsetLoading(false)from a stale run can end the loading state early.Add a cancellation flag and check it before each write. This also removes the React Doctor
no-set-state-after-await-in-effecterror from CI.π‘οΈ Proposed fix
useEffect(() => { + let cancelled = false; async function load() { setLoading(true); setError(false); try { const res = await getUserById(profileid); + if (cancelled) return; const u = res?.user || null; if (!u) { setError(true); setLoading(false); return; } setEducator(u); const [coursesData, booksData, spacesData, followersRes] = await Promise.allSettled([ fetchUserCourses(profileid), fetchUserBooks(profileid), fetchUserSpaces(profileid), getFollowersCount(profileid), ]); + if (cancelled) return;Add the same
if (cancelled) return;check after thecheckIfFollowingawait, wrap thefinallybody inif (!cancelled), and return the cleanup:load(); + return () => { + cancelled = true; + }; }, [profileid, currentUser?._id]);π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(pages)/educators/[profileid]/EducatorPageClient.jsx around lines 47 - 98, Update the useEffect load flow in EducatorPageClient to use a per-run cancelled flag, set it in the returned cleanup, and guard every state update after an awaitβincluding the user, courses, books, spaces, followers, and following writes. Also guard the catch and finally state updates so stale runs cannot set errors or loading, and check cancellation after checkIfFollowing resolves.Source: Linters/SAST tools
app/(pages)/(landingPage)/GlobalReach.jsx-3-3 (1)
3-3: π©Ί Stability & Availability | π Major | β‘ Quick winDeclare
cobeas a dependency.GlobalReach.jsximportscobe, but the rootpackage.jsondoes not declare it. Clean installs and production builds can fail.ArabesqueDivideris exported correctly.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(pages)/(landingPage)/GlobalReach.jsx at line 3, Declare cobe as a direct dependency in the root package manifest used by the project, ensuring the locked dependency metadata is updated consistently. Keep the existing createGlobe import in GlobalReach.jsx and the ArabesqueDivider export unchanged.components/molecules/dashboard/cards/educators/PublicCourseCard.jsx-57-66 (1)
57-66: π― Functional Correctness | π Major | β‘ Quick winUse public destinations for educator card links
app/dashboard/layout.jsxwraps these routes inProtectedRoute, which redirects unauthenticated visitors to/login. No public course, book, or space detail routes exist.Update all three links to public detail routes:
PublicCourseCard.jsx:62PublicBookCard.jsx:51PublicSpaceCard.jsx:87If the dashboard routes are intentional, preserve the destination in
ProtectedRoute; addingnextto these links alone is insufficient because the guard redirects to/loginwithout it.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/molecules/dashboard/cards/educators/PublicCourseCard.jsx` around lines 57 - 66, Update the detail links in PublicCourseCard.jsx (57-66), PublicBookCard.jsx (49-58), and PublicSpaceCard.jsx (83-91) to use the corresponding public course, book, and space destinations instead of protected dashboard routes. Do not add only a next parameter; ensure all three cards navigate directly to publicly accessible detail routes.components/organisms/dashboard/CommandPalette.jsx-248-265 (1)
248-265: π― Functional Correctness | π Major | β‘ Quick winRender the
reelandotherresult groups.
groupedResultsbuildsreelandotherbuckets, but the JSX renders onlycourse,book,user, andspace.typeLinks.reelexists, so reels are searchable yet invisible. If a query returns only reels or an unknown type,results.length > 0suppressesCommandEmptyat Line 306, and the palette shows just the "See all results" action. The user then sees an apparently empty result list.Add the missing groups after the Spaces group.
π Proposed fix
)} + + {/* Reels */} + {groupedResults.reel.length > 0 && ( + <CommandGroup heading="Reels"> + {groupedResults.reel.map((item) => { + const title = item.title || item.name || "Untitled Reel"; + return ( + <CommandItem + key={`reel-${item.id}`} + value={`reel-${item.id}-${title}`} + onSelect={() => + handleSelect(typeLinks.reel(item.id), title) + } + className="cursor-pointer" + > + {getTypeIcon("reel")} + <span className="truncate font-medium">{title}</span> + </CommandItem> + ); + })} + </CommandGroup> + )} </> )}Decide also how
groupedResults.othershould render, or drop the bucket if unknown types must be ignored.Also applies to: 402-430
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/dashboard/CommandPalette.jsx` around lines 248 - 265, Update the CommandPalette JSX rendering after the Spaces group to render groupedResults.reel using typeLinks.reel, and add an explicit rendering decision for groupedResults.other: either display unknown-type results through the existing result-item path or remove the other bucket and ignore those results. Ensure reel-only and unknown-type searches show visible results instead of only the βSee all resultsβ action.app/dashboard/ai/page.jsx-42-58 (1)
42-58: π©Ί Stability & Availability | π Major | β‘ Quick winAdd a cancellation flag to the chat-history effect.
The effect writes
setChatIdandsetMessagesafter two awaits. Ifuser?.idchanges, or if the user selects a chat in the sidebar while the requests are in flight, the late resolution overwrites the current conversation. The sync effect at Lines 61-66 sets the same state, so the two effects can race and the user can see a different chat than the one selected.Gate the setters behind an ignore flag.
π‘οΈ Proposed fix
useEffect(() => { + let ignore = false; const load = async () => { if (!user?.id) return; try { const data = await loadChatHistory(user.id, true); + if (ignore) return; if (data.chats?.length > 0) { const latestChat = data.chats[0]; - setChatId(latestChat.chat_id); const history = await loadChatMessages(latestChat.chat_id); + if (ignore) return; + setChatId(latestChat.chat_id); setMessages(history); } } catch (error) { console.error("Error loading user data:", error); } }; load(); + return () => { + ignore = true; + }; }, [user?.id]);π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/dashboard/ai/page.jsx` around lines 42 - 58, Update the chat-history useEffect around load so it tracks an ignore/cancellation flag, checks that flag before setChatId and setMessages after the awaited requests, and marks the flag during cleanup. Preserve the existing user?.id guard and ensure late results cannot overwrite a chat selected by the sidebar or a newer effect run.Source: Linters/SAST tools
components/atoms/dashboard/Notybell.jsx-85-89 (1)
85-89: π― Functional Correctness | π Major | β‘ Quick winFix invisible empty-state text.
Modal.jssets an explicitbg-whitebackground onDialogPrimitive.Content. The empty-state text here usestext-white/70, so it renders nearly invisible white-on-white whennotes.length === 0. Change the text color to a visible one, for exampletext-muted-foreground.π¨ Proposed fix
{notes.length === 0 && ( - <p className="text-center text-sm text-white/70"> + <p className="text-center text-sm text-muted-foreground"> Youβre all caught up! </p> )}π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/atoms/dashboard/Notybell.jsx` around lines 85 - 89, Update the empty-state paragraph rendered when notes.length === 0 in Notybell to use a visible text color such as text-muted-foreground instead of text-white/70, preserving the existing message and layout.components/organisms/dashboard/StatsOverview.jsx-51-77 (1)
51-77: π©Ί Stability & Availability | π Major | β‘ Quick winRetry button does not retry anything.
useStats()only fetches once, driven byuser?._id; it has no dependency onretryKeyand exposes norefetchfunction. TheretryKeystate added here is applied only to the success-branch grid (key={retryKey}on line 82), which is not rendered whileerroris true. Clicking "Retry" therefore does nothing observable:errorstays true and the same error UI keeps rendering. GiveuseStatsa way to re-run its fetch (acceptretryKeyas an effect dependency, or return arefetchcallback) so the button has an actual effect.π§ Suggested direction (requires updating hooks/useStats.js as well)
- const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error } = useStats(); + const { coursesEnrolled, booksRead, upcomingSessions, messagesUnread, totalUptime, loading, error, refetch } = useStats();<Button round className="bg-accent text-white text-sm" onClick={() => setRetryKey(k => k + 1)}> + <Button round className="bg-accent text-white text-sm" onClick={() => refetch()}>π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/dashboard/StatsOverview.jsx` around lines 51 - 77, Make the retry control in StatsOverview actually re-run the stats request: update useStats and its invocation from StatsOverview to accept retryKey as a fetch-effect dependency, or expose and call a refetch callback. Ensure clicking Retry causes the failed request to execute again and refreshes the loading/error state instead of only changing the success-branch grid key.components/organisms/auth/login-form.jsx-188-200 (1)
188-200: π Maintainability & Code Quality | π Major | β‘ Quick winKeyboard users cannot reach the unavailability tooltip.
stellarButtonis disabled whenstellarAuthAvailable === false. The tooltip trigger is the wrapping<span>at line 191. A plain<span>is not focusable, and a disabled<button>is removed from the tab order.The result: a keyboard or screen-reader user sees a dead button and never receives the explanation that the SEP-10 endpoint is not deployed.
Add
tabIndex={0}to the span so focus reaches the trigger. Radix Tooltip opens on focus. Rendering the same message as visible helper text below the button is an even more robust option, because it does not depend on hover or focus at all.βΏ Proposed fix
<TooltipTrigger asChild> - <span className="w-full inline-flex">{stellarButton}</span> + <span className="w-full inline-flex" tabIndex={0}> + {stellarButton} + </span> </TooltipTrigger>π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/organisms/auth/login-form.jsx` around lines 188 - 200, Add tabIndex={0} to the span wrapping stellarButton in the stellarAuthAvailable === false branch so keyboard users can focus the TooltipTrigger and receive the unavailable-message content, while preserving the existing disabled-button behavior.app/verify-email/page.jsx-21-38 (1)
21-38: π©Ί Stability & Availability | π Major | β‘ Quick winEncode the token and check the response status.
Three points on this fetch chain:
- Line 21 interpolates
tokenstraight into the URL path. If a token ever contains/,+,?, or#, the request targets the wrong path. Standard base64 contains/and+. Wrap it inencodeURIComponent.- The code never checks
res.ok. If the API returns a 500 with an HTML error page,res.json()throws and the user sees the generic "An error occurred" message instead of the real reason. Read the status first.- The fetch has no timeout. If the API hangs, the page stays on the spinner forever with no way out except a manual reload.
AbortSignal.timeoutgives the user a definite outcome.The effect also has no cancellation guard, so a fast unmount leaves a state update targeting an unmounted component. The abort controller in the fix below covers that too.
π§ Proposed fix
- fetch(`${config.apiUrl}/api/auth/verify-email/${token}`) - .then((res) => res.json()) + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 15000); + + fetch( + `${config.apiUrl}/api/auth/verify-email/${encodeURIComponent(token)}`, + { signal: controller.signal } + ) + .then(async (res) => { + const body = await res.json().catch(() => ({})); + if (!res.ok) { + throw new Error(body.message || `Verification failed (${res.status})`); + } + return body; + }) .then((data) => { if (data.success) { setMessage(data.message || "Email verified successfully!"); if (data.accessToken && data.user) { persistSession(data.accessToken, data.user); } setStatus("success"); } else { setStatus("error"); setMessage(data.message || "Verification failed."); } }) - .catch(() => { + .catch((err) => { + if (err.name === "AbortError") return; setStatus("error"); - setMessage("An error occurred. Please try again."); - }); + setMessage(err.message || "An error occurred. Please try again."); + }) + .finally(() => clearTimeout(timer)); + + return () => { + clearTimeout(timer); + controller.abort(); + }; }, [searchParams]);π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/verify-email/page.jsx` around lines 21 - 38, Update the verification fetch in the effect to wrap token with encodeURIComponent, use an AbortController timeout, and guard state updates after cancellation or unmount. Check res.ok before parsing JSON, preserving the API error message when available while handling aborted or other failures with the existing error state and message.app/verify-email/page.jsx-1-9 (1)
1-9: π©Ί Stability & Availability | π Major | β‘ Quick winWrap the
useSearchParamsconsumer in a Suspense boundary.Move the verification logic into an inner client component and render it inside
<Suspense>with the existing loading UI as the fallback. Without this boundary, the/verify-emailproduction build can fail during static prerendering.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/verify-email/page.jsx` around lines 1 - 9, Update VerifyEmailPage by moving the useSearchParams-dependent verification logic into an inner client component, then render that component inside a React Suspense boundary with the existing loading UI as its fallback; keep routing, session persistence, and verification behavior unchanged.Source: Path instructions
hooks/useAuth.js-7-7 (1)
7-7: π Security & Privacy | π Major | β‘ Quick winApply one secure cookie policy to every auth-cookie write.
COOKIE_OPTIONScovers onlypersistSession. The refresh andrefreshUserpaths still write cookies withoutsecureorsameSite. Reuse the shared options inlib/config/axios.config.jsandcomponents/providers/AuthProvider.jsx.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@hooks/useAuth.js` at line 7, Update COOKIE_OPTIONS in useAuth and reuse the same shared cookie options for every auth-cookie write, including persistSession, refresh, and refreshUser. Ensure the writes in axios.config.js and AuthProvider use options that include secure and sameSite, rather than maintaining separate incomplete option objects.components/stellar/StellarProvider.jsx-49-89 (1)
49-89: π― Functional Correctness | π Major | β‘ Quick winWallet-extension detection only covers Freighter.
hasWalletExtensionis set only fromwindow.freighter.isConnected(), at Line 74. The kit is initialized withFreighterModule,xBullModule, andAlbedoModuleat Lines 57-61. A user with only xBull or Albedo installed still getshasWalletExtension = false. Downstream components use this flag to decide between "Install Wallet" and "Connect Wallet" UI. Those users see an incorrect "install a wallet" prompt even though a supported wallet is already installed.Check whether the kit exposes a method to enumerate installed/supported wallets across all configured modules (the kit documentation references a method that "will let you know those the user have already installed/has access to"), and use it instead of checking
window.freighterdirectly.π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/stellar/StellarProvider.jsx` around lines 49 - 89, Update wallet detection in the `detectWallets` effect to use StellarWalletsKitβs installed/accessibile-wallet enumeration API across all configured modules (`FreighterModule`, `xBullModule`, and `AlbedoModule`) instead of checking `window.freighter.isConnected()`. Set `hasWalletExtension` true when any supported wallet is available, and preserve the existing false fallback and error handling when none are accessible or detection fails.components/stellar/PaymentModal.jsx-100-131 (1)
100-131: π― Functional Correctness | π Major | β‘ Quick winUser cancellation is shown as a hard failure.
executePaymentnow returns plainfalsewhen the user rejects signing (see hooks/useStellarPayment.js). At Line 108-115,handleConfirmtreats any falsy return as a generic failure and callssetStep("error")with "Transaction failed. Please try again." A user who cancels the wallet prompt sees a scary error screen, even though the hook already shows an info toast for the same cancellation. The dedicatedisUserRejection(err)handling in this function's owncatchblock, at Lines 121-125, is now unreachable, becauseexecutePaymentno longer throws for rejections.Fix the root cause in
executePayment's return contract (see hooks/useStellarPayment.js) so cancellations can be distinguished from genuine failures here, and route cancellations back to "preview" instead of "error".π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/stellar/PaymentModal.jsx` around lines 100 - 131, Update executePayment in useStellarPayment.js so user signing cancellations preserve a distinguishable rejection signal instead of returning plain false, allowing PaymentModalβs handleConfirm to route them through isUserRejection and back to the preview step. Keep genuine payment failures distinguishable so the existing generic error handling remains unchanged.components/stellar/StellarProvider.jsx-280-317 (1)
280-317: π― Functional Correctness | π Major | β‘ Quick winUse non-floating-point arithmetic for the balance check.
parseFloat(walletInfo.usdcBalance || 0) < (price || 0)at Line 306 compares USDC amounts using floating-point numbers. Floating-point representation can misround currency values at the boundary, which can incorrectly flag a sufficient balance as insufficient, or the reverse.Convert both sides to a fixed integer unit (for example, cents) before comparing, or use a decimal-safe comparison helper.
π° Proposed fix using integer-cent comparison
+ const toCents = (v) => Math.round(parseFloat(v || 0) * 100); if ( connectedWallet && walletInfo && - parseFloat(walletInfo.usdcBalance || 0) < (price || 0) + toCents(walletInfo.usdcBalance) < toCents(price) ) {As per path instructions, "Flag any deviation from this flow, any secret-key handling, and any amount arithmetic done with floating point."
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/stellar/StellarProvider.jsx` around lines 280 - 317, Replace the floating-point balance comparison in validateForPayment with decimal-safe fixed-unit integer arithmetic or the existing decimal comparison helper, converting both walletInfo.usdcBalance and price consistently before comparing. Preserve the current insufficient_balance issue and message behavior, while ensuring currency boundary values are compared exactly.Source: Path instructions
βΉοΈ Review info
βοΈ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 7595c4e2-b595-4921-b632-ca4df5b6b1d4
β Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
π Files selected for processing (84)
.env.exampleREADME.mdapp/(pages)/(landingPage)/GlobalReach.jsxapp/(pages)/educators/[profileid]/EducatorPageClient.jsxapp/(pages)/educators/[profileid]/page.jsxapp/account/profile/[profileid]/page.jsxapp/account/wallet/page.jsxapp/api/ai/chat/route.jsapp/api/ai/stream/route.jsapp/api/books/[bookId]/preview/route.jsapp/dashboard/ai/layout.jsapp/dashboard/ai/page.jsxapp/dashboard/courses/[courseId]/CourseDetailPageClient.jsxapp/dashboard/courses/[courseId]/page.jsxapp/dashboard/courses/page.jsxapp/dashboard/layout.jsxapp/dashboard/library/[bookid]/page.jsxapp/dashboard/library/page.jsxapp/dashboard/library/read/[bookid]/page.jsxapp/dashboard/search/[searchparam]/page.jsxapp/dashboard/spaces/[spacesid]/page.jsxapp/layout.jsapp/verify-email/page.jsxcomponents/DebugAuthLogs.jsxcomponents/atoms/dashboard/Notybell.jsxcomponents/atoms/dashboard/Searchbox.jsxcomponents/atoms/dashboard/vid-player-box.jsxcomponents/atoms/form/ComboBox.jsxcomponents/atoms/reels/ReelActionButton.jsxcomponents/molecules/Modal.jscomponents/molecules/dashboard/LibraryToolbar.jsxcomponents/molecules/dashboard/Notybell.jsxcomponents/molecules/dashboard/cards/courseCard.jsxcomponents/molecules/dashboard/cards/educators/PublicBookCard.jsxcomponents/molecules/dashboard/cards/educators/PublicCourseCard.jsxcomponents/molecules/dashboard/cards/educators/PublicSpaceCard.jsxcomponents/molecules/dashboard/cards/libraryCard.jsxcomponents/molecules/dashboard/cards/spaceCard.jsxcomponents/molecules/dashboard/nav-header.jsxcomponents/molecules/errors/NotFound.jsxcomponents/molecules/ladingpage/AnimatedSection.jsxcomponents/molecules/ladingpage/IslamicPattern.jsxcomponents/organisms/auth/login-form.jsxcomponents/organisms/auth/signup-form.jsxcomponents/organisms/create/book-create-form.jsxcomponents/organisms/create/course-create-form.jsxcomponents/organisms/create/space-create-form.jsxcomponents/organisms/dashboard/CommandPalette.jsxcomponents/organisms/dashboard/JaasMeetingClientSection.jsxcomponents/organisms/dashboard/LearningProgress.jsxcomponents/organisms/dashboard/StatsOverview.jsxcomponents/organisms/dashboard/UpcomingSessions.jsxcomponents/organisms/dashboard/ai/Ai-Sidebar.jsxcomponents/organisms/educators/EducatorProfileHeader.jsxcomponents/organisms/reels/ReelCard.jsxcomponents/organisms/reels/ReelFeed.jsxcomponents/providers/AppProviders.jsxcomponents/providers/AuthProvider.jsxcomponents/stellar/PaymentModal.jsxcomponents/stellar/StellarProvider.jsxcomponents/stellar/TransactionHistory.jsxcomponents/stellar/WalletConnectButton.jsxcomponents/stellar/WalletOnboarding.jsxcomponents/ui/command.jsxhooks/useAuth.jshooks/useCourseProgress.jshooks/useDebouncedValue.jshooks/useNotificationSSE.jshooks/useSearch.jshooks/useStellarAuth.jshooks/useStellarPayment.jshooks/useWalletReadiness.jslib/actions/ai/load-chat-history.jslib/actions/cached-api.jslib/config/axios.config.jslib/config/env.jslib/config/firebase.config.jslib/data.jslib/search-fallback.jslib/stellar/stellarErrors.jslib/utils/cloudinaryUpload.jsnext.config.mjspackage.jsonpublic/sw.js
π€ Files with no reviewable changes (6)
- hooks/useWalletReadiness.js
- components/organisms/reels/ReelFeed.jsx
- components/DebugAuthLogs.jsx
- components/stellar/WalletOnboarding.jsx
- package.json
- lib/data.js
| function PlayerProgressTracker({ onTimeUpdate, onEnded }) { | ||
| const player = useMediaPlayer(); | ||
|
|
||
| useEffect(() => { | ||
| if (!player) return; | ||
|
|
||
| const timeSub = player.on('time-update', (e) => { | ||
| if (onTimeUpdate) { | ||
| onTimeUpdate(e.currentTime, e.duration); | ||
| } | ||
| }); | ||
|
|
||
| const endedSub = player.on('ended', () => { | ||
| if (onEnded) { | ||
| onEnded(); | ||
| } | ||
| }); | ||
|
|
||
| return () => { | ||
| timeSub(); | ||
| endedSub(); | ||
| }; | ||
| }, [player, onTimeUpdate, onEnded]); | ||
|
|
||
| return null; | ||
| } |
There was a problem hiding this comment.
π― Functional Correctness | π΄ Critical | β‘ Quick win
π§© Analysis chain
π Web query:
Vidstack player time-update event detail currentTime duration subscribe API
π‘ Result:
In Vidstack Player, you can track media time and duration updates using events or state subscription hooks [1][2]. Event Handling The player dispatches specific DOM events that you can listen to for changes in time and duration [3][4]. 1. time-update Event: Fired periodically (approximately every 250ms) as the media plays or when the user seeks [3][5]. The event's detail contains the current playback time [3]. - Listener: player.addEventListener('time-update', (event) => { console.log(event.detail.currentTime); }); [5] 2. time-change Event: Fired when the currentTime property changes; it provides the "real" playback time without accounting for clipping [3]. 3. duration-change Event: Fired when the media's duration is determined or updated [3][5]. State Subscription For more efficient state managementβespecially in frameworks like ReactβVidstack provides hooks and subscription methods that avoid manual event binding [1]. - useMediaState: Use this hook to subscribe to specific pieces of state, such as currentTime or duration [1][6]. Example: const currentTime = useMediaState('currentTime', player); [7] - subscribe: You can call.subscribe directly on the player instance to watch for state changes [1][8]. This method returns an unsubscribe function to prevent memory leaks [9][8]. Example: player.subscribe(({ currentTime, duration }) => { console.log(currentTime, duration); }); [8] API Properties - currentTime: A number representing the current playback time in seconds [4][10]. Setting this value will seek the media to that position [4][10]. - duration: A number representing the total length of the media in seconds [4]. If the length is indefinite (e.g., live streams), it returns +Infinity [4]. It is recommended to use hooks like useMediaState or the player's subscribe method for state management to ensure clean, reactive updates within your application [1].
Citations:
- 1: https://vidstack.io/docs/player/core-concepts/state-management/
- 2: https://vidstack.io/docs/player/core-concepts/events/
- 3: https://github.com/vidstack/player/blob/main/packages/vidstack/src/core/api/media-events.ts
- 4: https://vidstack.io/docs/player/components/core/player/
- 5: https://context7.com/vidstack/player/llms.txt
- 6: https://vidstack.io/docs/player/api/hooks/use-media-state/
- 7: How can I subscribe to the current seek time?Β vidstack/player#1263
- 8: https://github.com/vidstack/examples/blob/main/player/svelte/tailwind-css/src/Player.svelte
- 9: https://vidstack.io/docs/player/components/core/provider/
- 10: https://github.com/vidstack/player/blob/main/packages/vidstack/src/core/api/player-state.ts
π Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- candidate file ---'
fd -i 'vid-player-box\.jsx$' . | while read -r f; do
wc -l "$f"
sed -n '1,110p' "$f"
done
printf '%s\n' '--- progress hook references ---'
fd -i 'useCourseProgress\.js$' . | while read -r f; do
wc -l "$f"
sed -n '130,190p' "$f"
done
printf '%s\n' '--- Vidstack dependency declarations ---'
rg -n --glob 'package.json' --glob 'package-lock.json' --glob 'yarn.lock' --glob 'pnpm-lock.yaml' \
'`@vidstack/react`|vidstack' .Repository: Deen-Bridge/dnb-frontend
Length of output: 5534
π Script executed:
#!/bin/bash
set -eu
printf '%s\n' '--- exact dependency version ---'
sed -n '6095,6135p' package-lock.json
printf '%s\n' '--- progress callback wiring ---'
rg -n -C 6 'reportProgress|onTimeUpdate|PlayerProgressTracker|startTime' \
components/atoms/dashboard/vid-player-box.jsx hooks/useCourseProgress.js
printf '%s\n' '--- relevant player implementation ---'
sed -n '110,280p' components/atoms/dashboard/vid-player-box.jsxRepository: Deen-Bridge/dnb-frontend
Length of output: 17721
π Web query:
Vidstack v1.12.13 media events time-update event.detail currentTime duration player.subscribe API
π‘ Result:
In Vidstack (v1.12.13 and related versions), the media playback state is managed through a reactive system where events and state subscriptions serve different purposes [1][2]. Media Events: time-update The time-update event is fired periodically (approximately every 250ms) as media playback progresses [3]. It is intended for tracking the displayed time in the UI [4][5]. Event Detail: The MediaTimeUpdateEventDetail interface includes the following properties: - currentTime: The current playback time in seconds [4]. - played: A TimeRanges object representing the time intervals that have been played [4][6]. Note: For receiving the real playback time without accounting for clipping (often called provider time), the time-change event is preferred over time-update [4][5]. State Management: player.subscribe API For tracking specific state changesβsuch as currentTime or durationβit is generally recommended to use the subscribe API rather than listening to events, as events are primarily for actions [1]. You can subscribe to the player state to receive updates reactively: player.subscribe((state) => { const { currentTime, duration } = state; console.log(Time: ${currentTime}, Duration: ${duration}); }); Alternatively, you can access current values directly via the player state properties: const currentTime = player.state.currentTime; const duration = player.state.duration; Key considerations: - State Subscription vs. Events: Use subscribe for tracking data (state changes) to ensure consistency, and use events for detecting user actions (e.g., play, pause, seek) [1]. - Tab Visibility: The time-update event may stop firing when the browser tab is inactive because Vidstack uses requestAnimationFrame for precise timing, which browsers typically throttle in background tabs [7]. If you require constant updates regardless of tab visibility, subscribing to state via the internal reactive system is generally more robust, though updates may still be limited by browser throttling of animation frames [7].
Citations:
- 1: Subscribe/events behaviorΒ vidstack/player#1352
- 2: https://vidstack.io/docs/player/core-concepts/state-management/
- 3: https://context7.com/vidstack/player/llms.txt
- 4: https://github.com/vidstack/player/blob/main/packages/vidstack/src/core/api/media-events.ts
- 5: https://vidstack.io/docs/player/components/core/player/
- 6: https://github.com/vidstack/player/blob/main/packages/vidstack/src/core/state/media-state-manager.ts
- 7:
onTimeUpdate-event only triggering when browser tab playing the video is in focusΒ vidstack/player#1332
π Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()
callback = re.search(
r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
player_file,
re.S,
)
assert callback, "time-update callback not found"
body = callback.group("body")
assert "e.currentTime" in body and "e.duration" in body
assert "e.detail.currentTime" not in body
# Model the documented Vidstack event shape and the hook's runtime guard.
event = {"detail": {"currentTime": 12.5, "played": object()}}
position = event.get("currentTime")
duration = event.get("duration")
assert position is None and duration is None
assert "typeof positionSeconds !== 'number'" in hook_file
assert "typeof durationSeconds !== 'number'" in hook_file
print("callback reads top-level event fields:", True)
print("documented event fields are under event.detail:", True)
print("both values fail the reportProgress number guard:", True)
PY
printf '%s\n' '--- all progress callback callers ---'
rg -n -C 5 'reportProgress|onTimeUpdate=|onEnded=' --glob '!package-lock.json' .Repository: Deen-Bridge/dnb-frontend
Length of output: 251
π Script executed:
#!/bin/bash
set +e
python3 - <<'PY'
from pathlib import Path
import re
player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()
callback = re.search(
r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
player_file,
re.S,
)
checks = [
("time-update callback found", callback is not None),
("top-level currentTime read", bool(callback and "e.currentTime" in callback.group("body"))),
("top-level duration read", bool(callback and "e.duration" in callback.group("body"))),
("detail currentTime absent", bool(callback and "e.detail.currentTime" not in callback.group("body"))),
("position number guard present", "typeof positionSeconds !== 'number'" in hook_file),
("duration number guard present", "typeof durationSeconds !== 'number'" in hook_file),
]
for label, result in checks:
print(f"{label}: {result}")
if callback:
print("--- callback body ---")
print(callback.group("body"))
print("--- all progress callback callers ---")
PY
rg -n -C 5 'reportProgress|onTimeUpdate=|onEnded=' --glob '!package-lock.json' .Repository: Deen-Bridge/dnb-frontend
Length of output: 6652
π Script executed:
#!/bin/bash
set -eu
python3 - <<'PY'
from pathlib import Path
import re
player_file = Path("components/atoms/dashboard/vid-player-box.jsx").read_text()
hook_file = Path("hooks/useCourseProgress.js").read_text()
callback = re.search(
r"player\.on\('time-update', \(e\) => \{(?P<body>.*?)\n\s*\}\);",
player_file,
re.S,
)
assert callback
body = callback.group("body")
assert "e.currentTime" in body and "e.duration" in body
# Vidstack's documented v1.12.13 event shape.
event = {"detail": {"currentTime": 12.5, "played": object()}}
position = event.get("currentTime")
duration = event.get("duration")
assert position is None and duration is None
# Match the actual hook guard, which uses double-quoted literals.
guard = re.search(
r'typeof positionSeconds !== "number".*?typeof durationSeconds !== "number"',
hook_file,
)
assert guard
print("current callback reads top-level fields:", True)
print("representative time-update event has no top-level values:", True)
print("both callback arguments are undefined:", True)
print("useCourseProgress rejects non-number arguments:", True)
PY
printf '%s\n' '--- completion fallback and progress state logic ---'
sed -n '1,125p' hooks/useCourseProgress.js
sed -n '155,235p' hooks/useCourseProgress.jsRepository: Deen-Bridge/dnb-frontend
Length of output: 6444
Subscribe to player state for progress values
The time-update event stores currentTime in event.detail and does not provide duration. The current reads pass undefined to reportProgress, so normal playback progress is never recorded. Use player.subscribe(({ currentTime, duration }) => ...) and report only when duration > 0.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/atoms/dashboard/vid-player-box.jsx` around lines 25 - 50, Update
PlayerProgressTracker to subscribe to player state with player.subscribe(({
currentTime, duration }) => ...), replacing the time-update handler; call
onTimeUpdate only when duration > 0 and pass the subscribed currentTime and
duration values, while preserving the ended subscription and cleanup.
| import React from "react"; | ||
| import { SidebarTrigger } from "@/components/ui/sidebar"; | ||
| import Notybell from "@/components/atoms/dashboard/Notybell"; | ||
| import Notybell from "@/components/molecules/dashboard/Notybell"; |
There was a problem hiding this comment.
π― Functional Correctness | π΄ Critical | β‘ Quick win
Import path points at the unwired SSE bell, not the live one.
components/molecules/dashboard/Notybell.jsx documents itself as "NOT YET WIRED UP" and states the bell currently rendered in the nav is components/atoms/dashboard/Notybell.jsx. This line imports from @/components/molecules/dashboard/Notybell, the molecules (SSE-backed) version, contradicting both files' own comments. Confirm which Notybell NavHeader must render. If the atoms version is still the intended live component, restore the import to @/components/atoms/dashboard/Notybell.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/molecules/dashboard/nav-header.jsx` at line 3, Update NavHeaderβs
Notybell import to use the live wired component from the atoms/dashboard
location, replacing the unwired molecules/dashboard import while leaving the
rendered component usage unchanged.
| const handleFileChange = (e) => { | ||
| const f = e.target.files[0]; | ||
| if (f) { | ||
| const result = validateFile(f, { maxSize: 100 * 1024 * 1024 }); | ||
| if (!result.valid) { setFileError(result.error); setFile(null); return; } | ||
| } | ||
| setFileError(''); | ||
| setFile(f || null); | ||
| }; |
There was a problem hiding this comment.
π― Functional Correctness | π΄ Critical | β‘ Quick win
Add allowedTypes for the book file. The default allowlist rejects documents.
validateFile defaults allowedTypes to ["image/*", "video/*"] (see lib/utils/cloudinaryUpload.js:128-159). The call at Line 57 passes only maxSize, so the default applies. A PDF or EPUB book file fails with "File type application/pdf is not allowed", and setFile(null) clears the selection. This blocks the primary upload path of the form.
Pass the document types that createBook accepts.
π Proposed fix
const handleFileChange = (e) => {
const f = e.target.files[0];
if (f) {
- const result = validateFile(f, { maxSize: 100 * 1024 * 1024 });
+ const result = validateFile(f, {
+ maxSize: 100 * 1024 * 1024,
+ allowedTypes: ['application/pdf', 'application/epub+zip'],
+ });
if (!result.valid) { setFileError(result.error); setFile(null); return; }
}π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const handleFileChange = (e) => { | |
| const f = e.target.files[0]; | |
| if (f) { | |
| const result = validateFile(f, { maxSize: 100 * 1024 * 1024 }); | |
| if (!result.valid) { setFileError(result.error); setFile(null); return; } | |
| } | |
| setFileError(''); | |
| setFile(f || null); | |
| }; | |
| const handleFileChange = (e) => { | |
| const f = e.target.files[0]; | |
| if (f) { | |
| const result = validateFile(f, { | |
| maxSize: 100 * 1024 * 1024, | |
| allowedTypes: ['application/pdf', 'application/epub+zip'], | |
| }); | |
| if (!result.valid) { setFileError(result.error); setFile(null); return; } | |
| } | |
| setFileError(''); | |
| setFile(f || null); | |
| }; |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/organisms/create/book-create-form.jsx` around lines 54 - 62,
Update the validateFile call inside handleFileChange to pass an allowedTypes
option containing the document MIME types accepted by createBook, while
retaining the existing 100 MB maxSize limit and validation flow so supported
PDF/EPUB selections are not rejected.
| const onSubmit = async (data) => { | ||
| try { | ||
| setLoading(true); | ||
| let thumbnailUrl = null; | ||
| let videoUrl = null; | ||
|
|
||
| // Upload thumbnail to Cloudinary first | ||
| if (thumbnail) { | ||
| toast.info("Uploading thumbnail..."); | ||
| thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail); | ||
| } | ||
|
|
||
| // Upload video to Cloudinary | ||
| if (video) { | ||
| toast.info("Uploading video... This may take a while."); | ||
| videoUrl = await videoUpload.uploadFile(video); | ||
| } | ||
|
|
||
| // Now create course with URLs | ||
| const data = await createCourse({ | ||
| form, | ||
| thumbnailUrl, | ||
| videoUrl, | ||
| category: form.category, | ||
| category: data.category, | ||
| }); | ||
|
|
||
| if (data && data.success) { | ||
| if (result && result.success) { | ||
| toast.success("Course created successfully!"); | ||
| router.push(`/dashboard/courses/${data.course._id}`); | ||
| router.push(`/dashboard/courses/${result.course._id}`); | ||
| } else { | ||
| toast.error(data?.message || "Failed to create course."); | ||
| toast.error(result?.message || "Failed to create course."); | ||
| } | ||
| } catch (error) { | ||
| toast.error( | ||
| error?.message || "An error occurred while creating the course." | ||
| ); | ||
| toast.error(error?.message || "An error occurred while creating the course."); | ||
| } finally { | ||
| setLoading(false); | ||
| thumbnailUpload.reset(); | ||
| videoUpload.reset(); | ||
| } | ||
| }; |
There was a problem hiding this comment.
π― Functional Correctness | π΄ Critical | β‘ Quick win
onSubmit does not compile, and it sends the wrong payload.
This block has four defects:
- Line 88 declares
const datain the same function scope as thedataparameter (Line 73). That is a redeclaration and raisesSyntaxError: Identifier 'data' has already been declared. The module never loads. - Line 89 passes
form(theuseFormreturn object) tocreateCourseinstead of the validated values. - Line 92 reads
data.categoryinside the initializer ofconst data, so it hits the temporal dead zone. - Lines 95-99 read
result, which no scope declares.
π Proposed fix
- const data = await createCourse({
- form,
- thumbnailUrl,
- videoUrl,
- category: data.category,
- });
-
- if (result && result.success) {
- toast.success("Course created successfully!");
- router.push(`/dashboard/courses/${result.course._id}`);
- } else {
- toast.error(result?.message || "Failed to create course.");
- }
+ const result = await createCourse({
+ form: data,
+ thumbnailUrl,
+ videoUrl,
+ category: data.category,
+ });
+
+ if (result?.success) {
+ toast.success("Course created successfully!");
+ router.push(`/dashboard/courses/${result.course._id}`);
+ } else {
+ toast.error(result?.message || "Failed to create course.");
+ }Confirm the parameter name that createCourse expects for the form values, because book-create-form.jsx uses form: data.
π Committable suggestion
βΌοΈ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const onSubmit = async (data) => { | |
| try { | |
| setLoading(true); | |
| let thumbnailUrl = null; | |
| let videoUrl = null; | |
| // Upload thumbnail to Cloudinary first | |
| if (thumbnail) { | |
| toast.info("Uploading thumbnail..."); | |
| thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail); | |
| } | |
| // Upload video to Cloudinary | |
| if (video) { | |
| toast.info("Uploading video... This may take a while."); | |
| videoUrl = await videoUpload.uploadFile(video); | |
| } | |
| // Now create course with URLs | |
| const data = await createCourse({ | |
| form, | |
| thumbnailUrl, | |
| videoUrl, | |
| category: form.category, | |
| category: data.category, | |
| }); | |
| if (data && data.success) { | |
| if (result && result.success) { | |
| toast.success("Course created successfully!"); | |
| router.push(`/dashboard/courses/${data.course._id}`); | |
| router.push(`/dashboard/courses/${result.course._id}`); | |
| } else { | |
| toast.error(data?.message || "Failed to create course."); | |
| toast.error(result?.message || "Failed to create course."); | |
| } | |
| } catch (error) { | |
| toast.error( | |
| error?.message || "An error occurred while creating the course." | |
| ); | |
| toast.error(error?.message || "An error occurred while creating the course."); | |
| } finally { | |
| setLoading(false); | |
| thumbnailUpload.reset(); | |
| videoUpload.reset(); | |
| } | |
| }; | |
| const onSubmit = async (data) => { | |
| try { | |
| let thumbnailUrl = null; | |
| let videoUrl = null; | |
| if (thumbnail) { | |
| toast.info("Uploading thumbnail..."); | |
| thumbnailUrl = await thumbnailUpload.uploadFile(thumbnail); | |
| } | |
| if (video) { | |
| toast.info("Uploading video... This may take a while."); | |
| videoUrl = await videoUpload.uploadFile(video); | |
| } | |
| const result = await createCourse({ | |
| form: data, | |
| thumbnailUrl, | |
| videoUrl, | |
| category: data.category, | |
| }); | |
| if (result?.success) { | |
| toast.success("Course created successfully!"); | |
| router.push(`/dashboard/courses/${result.course._id}`); | |
| } else { | |
| toast.error(result?.message || "Failed to create course."); | |
| } | |
| } catch (error) { | |
| toast.error(error?.message || "An error occurred while creating the course."); | |
| } finally { | |
| thumbnailUpload.reset(); | |
| videoUpload.reset(); | |
| } | |
| }; |
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/organisms/create/course-create-form.jsx` around lines 73 - 107,
Update onSubmit to avoid redeclaring its data parameter: pass the validated data
values as the createCourse payloadβs form field, following the
book-create-form.jsx convention, and derive category from those values. Store
the createCourse response in a separately named result variable, then use that
result consistently for success routing and error messaging.
Source: Linters/SAST tools
| <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" |
There was a problem hiding this comment.
π― Functional Correctness | π΄ Critical | β‘ Quick win
Remove the legacy form block. It breaks the build.
The return at Line 111 now contains two sibling elements: <Form> (Lines 112-183) and this <form>. JSX does not allow adjacent root elements, so Biome reports parse errors from Line 185 onward. This block also references identifiers that the rewrite removed: handleSubmit, handleChange, setForm, loading, and form.title. Label no longer appears in the import list either.
Delete Lines 184-280 so only the new <Form> tree remains. Please also run the linter locally before the next push, so CI stays green.
π§° Tools
πͺ Biome (2.5.5)
[error] 185-185: expected ) but instead found onSubmit
(parse)
[error] 193-193: expected , but instead found .
(parse)
[error] 196-196: Expected an expression but instead found '>'.
(parse)
[error] 202-202: expected , but instead found .
(parse)
[error] 206-206: Expected an expression but instead found '>'.
(parse)
[error] 210-210: expected , but instead found .
(parse)
[error] 211-211: Expected a property, a shorthand property, a getter, a setter, or a method but instead found '('.
(parse)
[error] 211-211: Expected a function body but instead found '=>'.
(parse)
[error] 212-212: expected , but instead found setForm
(parse)
[error] 212-212: Expected a parameter but instead found '('.
(parse)
[error] 212-212: expected , but instead found prev
(parse)
[error] 212-212: Expected a function body but instead found '=>'.
(parse)
[error] 212-212: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 214-214: unterminated regex literal
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
[error] 215-215: Expected a semicolon or an implicit semicolon after a statement, but found none
(parse)
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/organisms/create/course-create-form.jsx` around lines 184 - 217,
Remove the legacy native form block beginning at the second <form> after the new
<Form> tree and continuing through its closing content, leaving only the
rewritten <Form> return tree. This eliminates the adjacent JSX root and stale
references such as handleSubmit, handleChange, setForm, loading, form, and
Label; run the linter afterward to verify the file parses cleanly.
Source: Linters/SAST tools
| setUnreadCount((prev) => { | ||
| const deletedNotification = notifications.find( | ||
| const deletedNotification = prev.find( | ||
| (n) => n._id === data.notificationId | ||
| ); | ||
| return deletedNotification && !deletedNotification.isRead | ||
| ? prev - 1 | ||
| ? Math.max(0, prev - 1) | ||
| : prev; | ||
| }); |
There was a problem hiding this comment.
π©Ί Stability & Availability | π΄ Critical | β‘ Quick win
Fix the deleted-notification unread-count update.
setUnreadCount passes the numeric unread count as prev. At Line 89, prev.find(...) calls a list method on that number. A notification_deleted event will fail while React processes this updater.
Read the deleted item from the notification list, then update the numeric count. Use one reducer or a synchronized notification-list ref if concurrent events must use the latest list.
π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/useNotificationSSE.js` around lines 88 - 95, Fix the
notification_deleted updater in setUnreadCount so it does not call find on the
numeric unread count. Read the deleted notification from the maintained
notification list, then decrement the count only when that item exists and is
unread, preserving the Math.max(0, ...) guard; use a reducer or synchronized
list ref to ensure concurrent events read the latest list.
| if (user?._id) { | ||
| await refreshUser(user._id); | ||
| } | ||
|
|
||
| // Refresh wallet balance | ||
| await refreshBalance(); | ||
|
|
||
| setCurrentTransaction(null); | ||
| return res.data; | ||
| return { success: true, data: res.data }; | ||
| } else { | ||
| throw new Error(res.data.message); | ||
| } | ||
| } catch (error) { | ||
| const message = error.response?.data?.message || error.message; | ||
|
|
||
| // Handle specific Stellar errors | ||
| if (message.includes("insufficient") || message.includes("underfunded")) { | ||
| toast.error("Insufficient USDC balance"); | ||
| } else if (message.includes("op_no_trust") || message.includes("trustline")) { | ||
| toast.error( | ||
| "You need to add USDC trustline to your wallet first" | ||
| ); | ||
| } else if (message.includes("rejected") || message.includes("cancelled")) { | ||
| toast.error("Transaction was cancelled"); | ||
| if (isUserRejection(error) || error.code === "USER_REJECTED") { | ||
| toast.info("Transaction cancelled", { | ||
| description: "You declined the signing request. No changes were made.", | ||
| }); | ||
| return false; | ||
| } | ||
|
|
||
| const mapped = mapStellarError(error); | ||
| if (mapped) { | ||
| toast.error(mapped.title, { | ||
| description: mapped.nextStep, | ||
| }); | ||
| } else { | ||
| const message = error.response?.data?.message || error.message; | ||
| toast.error(`Payment failed: ${message}`); | ||
| } | ||
| return false; | ||
| return { success: false, cancelled: isCancelled }; |
There was a problem hiding this comment.
ποΈ Data Integrity & Integration | π΄ Critical | β‘ Quick win
isCancelled is undefined β this throws on every non-rejection payment failure.
At Line 107, return { success: false, cancelled: isCancelled }; references isCancelled, which is never declared in this function. Any payment failure that is not a user rejection (a Stellar operation error, a network failure, a backend rejection) reaches this line and throws a ReferenceError. That error escapes executePayment and propagates uncaught into PaymentModal.handleConfirm, which then shows the raw "isCancelled is not defined" message on the error screen instead of the intended mapped error.
This also exposes a second, related problem: the return shape of executePayment is inconsistent across its three exit paths:
- Success (Line 86):
{ success: true, data: res.data }. - User rejection (Line 95): plain
false. - Other failure (Line 107): a broken object that throws before it can be returned.
PaymentModal's success screen reads result?.transaction?.explorerUrl directly on the returned value, so the new { success: true, data } wrapper means that field is now at result.data.transaction.explorerUrl instead, and the "View on Stellar Explorer" link silently stops appearing after a successful payment.
Make the return contract consistent, and remove the undefined reference.
π Proposed fix for a consistent return contract
} catch (error) {
if (isUserRejection(error) || error.code === "USER_REJECTED") {
toast.info("Transaction cancelled", {
description: "You declined the signing request. No changes were made.",
});
- return false;
+ return { success: false, cancelled: true };
}
const mapped = mapStellarError(error);
if (mapped) {
toast.error(mapped.title, {
description: mapped.nextStep,
});
} else {
const message = error.response?.data?.message || error.message;
toast.error(`Payment failed: ${message}`);
}
- return { success: false, cancelled: isCancelled };
+ return { success: false, cancelled: false };π€ Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@hooks/useStellarPayment.js` around lines 80 - 107, Update executePayment so
every exit path returns a consistent result object: represent user rejection and
other failures with success false, and replace the undefined isCancelled
reference with the appropriate cancellation value or omit it. Preserve mapped
error handling and ensure the success response exposes transaction data where
PaymentModal expects result.transaction.explorerUrl, rather than nesting it
under result.data.
Summary by CodeRabbit
New Features
Bug Fixes
Documentation