React joyride addition - #12
Conversation
…ard navigation with data attributes
…earch functionality
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThe PR adds responsive student and vendor onboarding tours with route-aware navigation, replay controls, persistent completion state, and dashboard targets. It also updates materials filtering, vendor search caching, and basic-tier analytics rendering. ChangesDashboard onboarding tour
Materials and vendor data flows
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant DashboardLayout
participant OnboardingTour
participant useTourStore
participant Joyride
participant Router
DashboardLayout->>OnboardingTour: render tour context
OnboardingTour->>useTourStore: start selected tour
OnboardingTour->>Joyride: render controlled step
Joyride->>OnboardingTour: report navigation event
OnboardingTour->>Router: navigate to step route
Router->>OnboardingTour: provide updated pathname
OnboardingTour->>useTourStore: update pending step
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/dashboard/materials/page.tsx (1)
73-79: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick winPropagate errors from the
material_savesquery.When
showSavedOnlyis true and the saved-material query fails, the code treatsdataas an empty list and then shows the empty-state path. Throw the query error instead of mapping it to an empty save list.Proposed fix
- const { data: savedRows } = await supabaseAdmin + const { data: savedRows, error: savedRowsError } = await supabaseAdmin .from("material_saves") .select("material_id") .eq("user_id", user.id); + + if (savedRowsError) throw savedRowsError;🤖 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 `@src/app/dashboard/materials/page.tsx` around lines 73 - 79, Update the material_saves query in the dashboard page’s saved-material loading flow to capture and check its error result before assigning savedMaterialIds. When the query fails, propagate the error instead of defaulting savedRows to an empty list; retain the existing mapping behavior for successful queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/components/dashboard/mobile-header-menu.tsx`:
- Around line 25-31: Update the tour replay click handler to read the current
isStudent cookie immediately before determining the dashboard mode, instead of
relying solely on the cached isVendorViewRef value. Apply the same change to the
corresponding replay handler in TourHelpButton, preserving the existing
isVendorAccount and hasVendor logic while ensuring the current cookie controls
the tour mode after DashboardToggle changes it.
In `@src/components/materials/materials-filters.tsx`:
- Around line 96-105: Update the debounce effect around updateFilters and
handleMaterialSearch so a pending timer cannot apply search changes using stale
searchParams after bookmark or filter URL state changes. Reconcile or cancel the
timer when route state changes, or read the current search params inside the
timeout callback, while preserving the existing 500 ms debounce behavior.
- Around line 87-94: Make the initial-render guard in the debounced search use a
Strict Mode-safe user-change check instead of relying on the one-time
isFirstRender ref. Before router.push and handleMaterialSearch, compare the
current query with the URL value or require an explicit user-change flag, while
preserving the 500 ms debounce for genuine user edits.
In `@src/components/tour/onboarding-tour.tsx`:
- Around line 120-123: Update the tour-start flow around the forceStart check
and start(kind) so the tourStart query parameter is consumed after triggering a
forced start, or protected by a one-time ref. Ensure a completed or skipped tour
cannot restart immediately while preserving the initial forced-start behavior.
- Around line 244-259: Extend the tour callback handling in the onboarding tour
component so EVENTS.TARGET_NOT_FOUND follows the same controlled next-step
routing and advancement logic currently implemented for EVENTS.STEP_AFTER,
including boundary completion, pending route handling, and setStepIndex updates;
alternatively, explicitly stop the tour for missing targets.
- Around line 52-55: Update useIsMobile to use useSyncExternalStore with a
stable server snapshot and media-query subscription, rather than reading window
during initial state initialization. In the tour component, defer reading
tourDebug until after mount so debugOn and the derived steps/markup remain
identical during hydration, then update to the client value.
In `@src/components/tour/tour-steps.ts`:
- Around line 126-130: Ensure every referenced tour target remains mounted
across populated and empty states. In src/components/tour/tour-steps.ts lines
126-130, 147-151, 259-264, and 280-285, retain the materials and vendors steps
only when their targets exist in every state; in
src/components/materials/materials-content.tsx line 97, move
data-tour="page-materials" to a wrapper containing the empty state; and in
src/app/dashboard/vendors/page.tsx line 110, move data-tour="page-vendors" to a
wrapper containing the empty state.
In `@src/components/tour/tour-store.ts`:
- Line 4: Replace the relative tour imports with `@/`* aliases throughout the
affected files: update TourKind in src/components/tour/tour-store.ts:4, the
store and step-definition imports in
src/components/tour/onboarding-tour.tsx:13-21, and the store import in
src/components/tour/tour-help-button.tsx:6. Preserve the imported symbols and
behavior.
In `@src/utils/cache/vendors.ts`:
- Around line 34-50: Define a shared explicit vendor result type using
Database['public']['Tables']['vendors'] together with the joined
vendor_categories and profiles shapes, and apply it to getVendors’ return type.
Replace the any-based query typing and casts with this result shape, then
explicitly type fetchVendorFeed, getCachedVendors, and getVendorSearch so their
exported APIs return the typed vendor results.
In `@src/utils/supabase/queries/materials.ts`:
- Around line 59-61: Update the ids handling around the query builder so an
empty ids array immediately returns an empty result, while the existing
query.in("id", ids) call runs only for non-empty arrays. Preserve the current
behavior when ids is absent or contains values.
---
Outside diff comments:
In `@src/app/dashboard/materials/page.tsx`:
- Around line 73-79: Update the material_saves query in the dashboard page’s
saved-material loading flow to capture and check its error result before
assigning savedMaterialIds. When the query fails, propagate the error instead of
defaulting savedRows to an empty list; retain the existing mapping behavior for
successful queries.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: a1aa9a9e-2255-4516-8077-a598d00e283c
⛔ Files ignored due to path filters (1)
dev-server.logis excluded by!**/*.log
📒 Files selected for processing (29)
package.jsonsrc/app/dashboard/announcements/page.tsxsrc/app/dashboard/cgpa/page.tsxsrc/app/dashboard/layout.tsxsrc/app/dashboard/materials/page.tsxsrc/app/dashboard/notifications/page.tsxsrc/app/dashboard/profile/page.tsxsrc/app/dashboard/settings/page.tsxsrc/app/dashboard/subscription/page.tsxsrc/app/dashboard/vendors/analytics/page.tsxsrc/app/dashboard/vendors/page.tsxsrc/components/dashboard/DashboardQuickActions.tsxsrc/components/dashboard/DashboardQuickStats.tsxsrc/components/dashboard/DashboardWelcomeHeader.tsxsrc/components/dashboard/dashboard-toggle.tsxsrc/components/dashboard/mobile-bottom-nav.tsxsrc/components/dashboard/mobile-header-menu.tsxsrc/components/dashboard/vendor-dashboard.tsxsrc/components/materials/materials-content.tsxsrc/components/materials/materials-filters.tsxsrc/components/tour/onboarding-tour.tsxsrc/components/tour/tour-help-button.tsxsrc/components/tour/tour-steps.tssrc/components/tour/tour-store.tssrc/components/vendors/analytics-dashboard.tsxsrc/components/vendors/vendor-mobile-bottom-nav.tsxsrc/components/vendors/vendor-sidebar.tsxsrc/utils/cache/vendors.tssrc/utils/supabase/queries/materials.ts
| useEffect(() => { | ||
| const isStudent = document.cookie | ||
| .split('; ') | ||
| .find((r) => r.startsWith('isStudent=')) | ||
| ?.split('=')[1] !== 'false'; | ||
| isVendorViewRef.current = isVendorAccount || (hasVendor && !isStudent); | ||
| }, [hasVendor, isVendorAccount]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Read the dashboard mode when the user starts the tour.
After DashboardToggle changes the isStudent cookie, hasVendor and isVendorAccount stay unchanged. This effect does not rerun, so isVendorViewRef can retain the prior mode. A user who switches to vendor mode can then start the student tour.
Read the cookie in the replay click handler. Apply the same change to src/components/tour/tour-help-button.tsx, which uses the same cached-ref pattern.
Proposed fix
-import { useEffect, useRef } from 'react';
import Link from 'next/link';
@@
const start = useTourStore((s) => s.start);
- const isVendorViewRef = useRef(false);
- useEffect(() => {
+ const handleReplayTour = () => {
const isStudent = document.cookie
.split('; ')
.find((r) => r.startsWith('isStudent='))
?.split('=')[1] !== 'false';
- isVendorViewRef.current = isVendorAccount || (hasVendor && !isStudent);
- }, [hasVendor, isVendorAccount]);
+ start(isVendorAccount || (hasVendor && !isStudent) ? 'vendor' : 'student');
+ };
@@
- <DropdownMenuItem onClick={() => start(isVendorViewRef.current ? 'vendor' : 'student')}>
+ <DropdownMenuItem onClick={handleReplayTour}>🤖 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 `@src/components/dashboard/mobile-header-menu.tsx` around lines 25 - 31, Update
the tour replay click handler to read the current isStudent cookie immediately
before determining the dashboard mode, instead of relying solely on the cached
isVendorViewRef value. Apply the same change to the corresponding replay handler
in TourHelpButton, preserving the existing isVendorAccount and hasVendor logic
while ensuring the current cookie controls the tour mode after DashboardToggle
changes it.
| // Debounced search | ||
| const isFirstRender = useRef(true); | ||
|
|
||
| useEffect(() => { | ||
| if (isFirstRender.current) { | ||
| isFirstRender.current = false; | ||
| return; | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n 'reactStrictMode|<StrictMode' \
--glob 'next.config.*' \
--glob '**/*.{js,jsx,ts,tsx}' .Repository: royal334/my-school-and-I
Length of output: 162
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== matching file =="
git ls-files | grep 'src/components/materials/materials-filters\.tsx' || true
echo "== file outline/section =="
wc -l src/components/materials/materials-filters.tsx
sed -n '1,180p' src/components/materials/materials-filters.tsx
echo "== package/framework hints =="
for f in package.json next.config.js next.config.ts next.config.mjs; do
[ -f "$f" ] && { echo "--- $f"; sed -n '1,220p' "$f"; }
done
echo "== StrictMode references all tracked files =="
rg -n 'strictMode|StrictMode|useEffect|push|track|useRouter|useSearchParams' --glob '*.{ts,tsx,js,jsx,json}' . 2>/dev/null | head -200Repository: royal334/my-school-and-I
Length of output: 26725
Make the initial-render guard Strict Mode-safe.
Next.js dev mode enables React Strict Mode by default, and this effect keeps the first setup/cleanup as the initial-render guard. The second effect setup can then run before the first real setup, and its 500 ms timer can fire from an initial non-empty search. Use an explicit user-change flag, or compare the new query with the current URL before calling router.push and handleMaterialSearch.
🤖 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 `@src/components/materials/materials-filters.tsx` around lines 87 - 94, Make
the initial-render guard in the debounced search use a Strict Mode-safe
user-change check instead of relying on the one-time isFirstRender ref. Before
router.push and handleMaterialSearch, compare the current query with the URL
value or require an explicit user-change flag, while preserving the 500 ms
debounce for genuine user edits.
Source: MCP tools
| const timer = setTimeout(() => { | ||
| updateFilters(); | ||
| if (search.trim()) { | ||
| handleMaterialSearch(search.trim()); | ||
| } | ||
| }, 500); | ||
|
|
||
| return () => clearTimeout(timer); | ||
| // eslint-disable-next-line react-hooks/exhaustive-deps | ||
| }, [search, level, semester, type]); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate file =="
git ls-files | rg '(^|/)materials-filters\.tsx$|materials-filters\.tsx$' || true
echo "== file outline =="
if [ -f src/components/materials/materials-filters.tsx ]; then
ast-grep outline src/components/materials/materials-filters.tsx || true
echo "== relevant sections =="
wc -l src/components/materials/materials-filters.tsx
sed -n '1,180p' src/components/materials/materials-filters.tsx | cat -n
echo "== usages of handleMaterialSearch/updateFilters/saved/bookmark/clear =="
rg -n "handleMaterialSearch|updateFilters|saved|Bookmarks|bookmark|Clear|clear|searchParams|URLSearchParams|useRouter" src/components/materials/materials-filters.tsx src 2>/dev/null || true
fiRepository: royal334/my-school-and-I
Length of output: 27989
Avoid stale debounce updates that rewrite later URL changes.
updateFilters() uses the searchParams value from the render that started the 500 ms timer. If the user changes Search and then toggles Bookmarks or clears the search within that window, the timer re-rends the URL with the old flags, which can drop saved=true or restore cleared filter state. Cancel or reconcile the pending timer when the route state changes, or read the current search params when the debounce fires.
🤖 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 `@src/components/materials/materials-filters.tsx` around lines 96 - 105, Update
the debounce effect around updateFilters and handleMaterialSearch so a pending
timer cannot apply search changes using stale searchParams after bookmark or
filter URL state changes. Reconcile or cancel the timer when route state
changes, or read the current search params inside the timeout callback, while
preserving the existing 500 ms debounce behavior.
Source: MCP tools
| function useIsMobile() { | ||
| const [isMobile, setIsMobile] = useState(() => | ||
| typeof window !== 'undefined' && window.matchMedia('(max-width: 767.98px)').matches, | ||
| ); |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the target file and relevant lines without executing repo code.
if [ -f src/components/tour/onboarding-tour.tsx ]; then
echo "FOUND src/components/tour/onboarding-tour.tsx"
wc -l src/components/tour/onboarding-tour.tsx
echo "--- outline ---"
ast-grep outline src/components/tour/onboarding-tour.tsx --view compact || true
echo "--- lines 1-80 ---"
sed -n '1,90p' src/components/tour/onboarding-tour.tsx | nl -ba
echo "--- lines 240-285 ---"
sed -n '240,285p' src/components/tour/onboarding-tour.tsx | nl -ba
else
echo "TARGET FILE NOT FOUND"
git ls-files | rg 'onboarding-tour\.tsx$|src/components/tour'
fi
echo "--- search tourDebug/useIsMobile usages ---"
rg -n "useIsMobile|tourDebug|Joyride|Step|steps" src/components/tour/onboarding-tour.tsx src -g '!**/node_modules' || trueRepository: royal334/my-school-and-I
Length of output: 479
🏁 Script executed:
#!/bin/bash
set -euo pipefail
TARGET="src/components/tour/onboarding-tour.tsx"
echo "FOUND $TARGET"
wc -l "$TARGET"
echo "--- outline ---"
ast-grep outline "$TARGET" --view signatures || ast-grep outline "$TARGET" --view names || true
echo "--- lines 1-80 ---"
awk '{printf "%4d %s\n", NR, $0}' "$TARGET" | sed -n '1,80p'
echo "--- lines 240-285 ---"
awk '{printf "%4d %s\n", NR, $0}' "$TARGET" | sed -n '240,285p'
echo "--- search tourDebug/useIsMobile/joyride usages ---"
rg -n "useIsMobile|tourDebug|Joyride|steps|Step|useTour" "$TARGET" || true
echo "--- package hints for next/react versions ---"
for f in package.json pnpm-lock.yaml package-lock.json yarn.lock bun.lockb; do
[ -e "$f" ] && { echo "--- $f ---"; sed -n '1,120p' "$f"; }
doneRepository: royal334/my-school-and-I
Length of output: 13037
🌐 Web query:
Next.js React.hydrate mismatch browser-only values server returned false while rendering text "true"
💡 Result:
A hydration mismatch error like "Text content does not match server-rendered HTML" occurs when the initial HTML generated by the server differs from the HTML generated during the first render in the browser [1][2][3]. Because React requires these two renders to be identical for successful hydration, it throws this error when it detects a discrepancy [1][4][2]. Common causes include: - Accessing browser-only APIs (e.g., window, localStorage, navigator) during the initial render phase [5][6][7]. - Using non-deterministic values that change between server and client (e.g., Date.now, Math.random) [6][8][3]. - Differences in locale or timezone formatting between the server and the user's browser [5][8][9]. - Conditional rendering based on the presence of window (e.g., typeof window !== 'undefined') that produces different content [6][1][9]. - Invalid HTML nesting (e.g.,
tag) [1][8][9]. - Browser extensions (like password managers or ad blockers) that inject content into the DOM before hydration [8][9][3]. Recommended solutions: 1. Use useEffect for client-only logic: Perform actions that depend on browser-only APIs or dynamic data inside a useEffect hook [10][6][7]. By initializing state to a consistent value (like null or a loading state) and updating it only after the component mounts, you ensure the server and initial client renders match [11][6][4]. 2. Suppress unavoidable mismatches: If a mismatch is legitimate and unavoidable—such as a dynamic timestamp—add the suppressHydrationWarning={true} prop to the specific element [1][9][3]. This is an "escape hatch" that silences the warning for that element only; it should not be used as a blanket fix for rendering logic bugs [1][3][7]. 3. Disable SSR for specific components: If a component is entirely client-side, you can skip server-side rendering for it using dynamic imports: const ClientComponent = dynamic( => import('./ClientComponent'), { ssr: false }); [12][1][7] 4. Check for invalid HTML nesting: Ensure your components do not contain invalid HTML structure, as this often causes hydration failures [1][9]. Avoid using suppressHydrationWarning on high-level components or as a way to hide underlying logic issues, as this can mask other bugs and lead to inconsistent UI behavior [9][3][7].
Citations:
- 1: https://nextjs.org/docs/messages/react-hydration-error
- 2: https://www.netlify.com/blog/fix-next-js-react-hydration-error/
- 3: https://engahmed.hashnode.dev/debugging-hydration-mismatches-in-the-next-js-app-router-3e99c5
- 4: https://stackoverflow.com/questions/72673362/error-text-content-does-not-match-server-rendered-html
- 5: https://blog.logrocket.com/how-fix-rsc-hydration-mismatches-next-js/
- 6: What is hydration in Next js vercel/next.js#93911
- 7: https://markaicode.com/errors/nextjs-common-errors-and-fixes/
- 8: Returning a string from a page component causes hydration mismatch error vercel/next.js#65985
- 9: https://snipshift.dev/blog/fix-hydration-mismatch-nextjs
- 10: https://github.com/vercel/nextjs-skills/blob/HEAD/skills/next-best-practices/hydration-error.md
- 11: https://stackoverflow.com/questions/71706064/react-18-hydration-failed-because-the-initial-ui-does-not-match-what-was-render
- 12: Hydration failed because the initial UI does not match what was rendered on the server vercel/next.js#35773
Do not derive initial render state from browser-only values.
useIsMobile() renders false during SSR but the browser viewport may be narrow on the first client render, and debugOn does the same when tourDebug is present. Use a stable server snapshot for the media query with useSyncExternalStore, and read tourDebug only after mount to avoid different steps/debug markup during hydration.
🧰 Tools
🪛 React Doctor (0.9.3)
[error] 54-54: matchMedia() in a useState initializer can cause an SSR crash or seed different server and hydration state. Prefer CSS media queries for layout, or use useSyncExternalStore with a stable server snapshot.
Prefer CSS media queries for layout, or subscribe with useSyncExternalStore and provide a stable server snapshot.
(no-match-media-in-state-initializer)
🤖 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 `@src/components/tour/onboarding-tour.tsx` around lines 52 - 55, Update
useIsMobile to use useSyncExternalStore with a stable server snapshot and
media-query subscription, rather than reading window during initial state
initialization. In the tour component, defer reading tourDebug until after mount
so debugOn and the derived steps/markup remain identical during hydration, then
update to the client value.
Source: Linters/SAST tools
| const forceStart = | ||
| typeof window !== 'undefined' && | ||
| new URLSearchParams(window.location.search).has('tourStart'); | ||
| if (hasSeenTour(kind, userId) && !forceStart) return; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Consume tourStart after the forced start.
After a completed or skipped tour, stop() sets run to false. Because tourStart remains in the URL, this effect immediately starts the tour again. Remove the parameter or guard it with a one-time ref before calling start(kind).
🤖 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 `@src/components/tour/onboarding-tour.tsx` around lines 120 - 123, Update the
tour-start flow around the forceStart check and start(kind) so the tourStart
query parameter is consumed after triggering a forced start, or protected by a
one-time ref. Ensure a completed or skipped tour cannot restart immediately
while preserving the initial forced-start behavior.
| if (type === EVENTS.STEP_AFTER) { | ||
| const delta = action === ACTIONS.PREV ? -1 : 1; | ||
| const nextIndex = index + delta; | ||
| if (nextIndex < 0 || nextIndex >= stepsRef.current.length) { | ||
| if (tour) markTourSeen(tour, userId); | ||
| stop(); | ||
| return; | ||
| } | ||
| const nextStep = stepsRef.current[nextIndex]; | ||
| if (nextStep.route && nextStep.route !== pathnameRef.current) { | ||
| setPending(nextStep.route, nextIndex); | ||
| router.push(nextStep.route); | ||
| } else { | ||
| setStepIndex(nextIndex); | ||
| } | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Find file and relevant imports/usages:"
fd -a 'onboarding-tour\.tsx$' . | sed 's#^\./##' || true
file="$(fd 'onboarding-tour\.tsx$' . | head -n1 || true)"
if [ -n "$file" ]; then
echo
echo "File: $file"
wc -l "$file"
echo
echo "Outline:"
ast-grep outline "$file" --view compact || true
echo
echo "Relevant lines 200-280:"
sed -n '200,280p' "$file" | nl -ba -v 200
echo
echo "All Event/Action refs:"
rg -n "EVENTS|ACTIONS|TARGET_NOT_FOUND|STEP_AFTER|targetNotFound|joyride|useJoyride|useState|stepsRef|markTourSeen|setStepIndex" "$file" || true
fi
echo
echo "Search Joyride event usage in repo:"
rg -n "EVENTS\.TARGET_NOT_FOUND|TARGET_NOT_FOUND|joyride|useJoyride" src package.json || trueRepository: royal334/my-school-and-I
Length of output: 582
🏁 Script executed:
#!/bin/bash
set -euo pipefail
file="./src/components/tour/onboarding-tour.tsx"
echo "File: $file"
wc -l "$file"
echo
echo "Outline:"
ast-grep outline "$file" --view names || true
echo
echo "Relevant lines 1-320:"
awk '{printf "%5d\t%s\n", NR, $0}' "$file" | sed -n '1,320p'
echo
echo "All Event/Action references in file:"
rg -n "EVENTS|ACTIONS|TARGET_NOT_FOUND|STEP_AFTER|TARGET|stepsRef|markTourSeen|setStepIndex|setPending|router\.push" "$file" || true
echo
echo "Search Joyride event usage in repo:"
rg -n "EVENTS\.TARGET_NOT_FOUND|TARGET_NOT_FOUND|useJoyride|targetNotFound" src package.json || trueRepository: royal334/my-school-and-I
Length of output: 13875
🌐 Web query:
react-joyride target_not_found event controlled tour EVENTS.TARGET_NOT_FOUND
💡 Result:
In React Joyride, the EVENTS.TARGET_NOT_FOUND ('error:target_not_found') event is triggered when the library cannot locate a step's target element in the DOM after attempting to find it [1][2]. Important Considerations for Controlled Tours: 1. Handling the Event: You can listen for this event in your onEvent callback to manually advance or adjust the tour [1][2]. In controlled mode—where you manage the stepIndex via state—the library expects you to explicitly handle the navigation index [1][2]. 2. Controlled Mode Behavior: It is important to note that if you are using controlled mode (by providing a stepIndex prop), you are responsible for updating the stepIndex when this event occurs [2][3]. If your tour relies on conditional UI (like opening a sidebar or navigating to a new route), the recommended approach is to use the before step hook instead of relying solely on the TARGET_NOT_FOUND event [4][5]. The before hook allows you to trigger the necessary state changes to ensure the target exists before Joyride attempts to render the step [4][5]. 3. Target Availability: Joyride automatically waits for a target to appear for a duration defined by targetWaitTimeout (default is 1000ms) before firing the TARGET_NOT_FOUND event [4]. 4. Implementation: A common pattern for handling this in an onEvent callback is: if ([EVENTS.STEP_AFTER, EVENTS.TARGET_NOT_FOUND].includes(type)) { setStepIndex(index + (action === ACTIONS.PREV? -1: 1)); } This allows your tour to continue even if a specific target could not be found [1][2]. For more complex scenarios, you may need to conditionally update your component state or trigger UI changes within this handler to resolve the missing target before setting the next index [6][3]. Top Results: [1][2][4]
Citations:
- 1: https://react-joyride.com/docs/events
- 2: https://react-joyride.com/docs/how-it-works
- 3: https://github.com/gilbarbara/react-joyride-demo/blob/master/src/Controlled/index.tsx
- 4: https://react-joyride.com/docs/recipes
- 5: https://react-joyride.com/docs/props
- 6: trying to deal with possible target errors gilbarbara/react-joyride#1100
Handle missing targets in controlled mode.
EVENTS.TARGET_NOT_FOUND is emitted when Joyride cannot find a step target, and controlled tours must update stepIndex manually. Apply the same next-step routing/advancement for STEP_AFTER and TARGET_NOT_FOUND, or explicitly stop the tour.
🤖 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 `@src/components/tour/onboarding-tour.tsx` around lines 244 - 259, Extend the
tour callback handling in the onboarding tour component so
EVENTS.TARGET_NOT_FOUND follows the same controlled next-step routing and
advancement logic currently implemented for EVENTS.STEP_AFTER, including
boundary completion, pending route handling, and setStepIndex updates;
alternatively, explicitly stop the tour for missing targets.
| target: pageTarget('[data-tour="page-materials"]'), | ||
| content: 'Access lecture notes, past questions, and study materials uploaded by verified students.', | ||
| title: 'Materials Library', | ||
| route: '/dashboard/materials', | ||
| placement: 'top', |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep every referenced page target mounted.
src/components/tour/tour-steps.ts#L126-L130: retain the materials step only ifpage-materialsexists in every materials state.src/components/tour/tour-steps.ts#L147-L151: retain the vendors step only ifpage-vendorsexists in every vendor state.src/components/tour/tour-steps.ts#L259-L264: apply the same availability rule to the mobile materials step.src/components/tour/tour-steps.ts#L280-L285: apply the same availability rule to the mobile vendors step.src/components/materials/materials-content.tsx#L97-L97: movedata-tour="page-materials"to a wrapper that also contains the empty state.src/app/dashboard/vendors/page.tsx#L110-L110: movedata-tour="page-vendors"to a wrapper that also contains the empty state.
📍 Affects 3 files
src/components/tour/tour-steps.ts#L126-L130(this comment)src/components/tour/tour-steps.ts#L147-L151src/components/tour/tour-steps.ts#L259-L264src/components/tour/tour-steps.ts#L280-L285src/components/materials/materials-content.tsx#L97-L97src/app/dashboard/vendors/page.tsx#L110-L110
🤖 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 `@src/components/tour/tour-steps.ts` around lines 126 - 130, Ensure every
referenced tour target remains mounted across populated and empty states. In
src/components/tour/tour-steps.ts lines 126-130, 147-151, 259-264, and 280-285,
retain the materials and vendors steps only when their targets exist in every
state; in src/components/materials/materials-content.tsx line 97, move
data-tour="page-materials" to a wrapper containing the empty state; and in
src/app/dashboard/vendors/page.tsx line 110, move data-tour="page-vendors" to a
wrapper containing the empty state.
| 'use client'; | ||
|
|
||
| import { create } from 'zustand'; | ||
| import type { TourKind } from './tour-steps'; |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Use @/* imports throughout the tour feature.
src/components/tour/tour-store.ts#L4-L4: importTourKindfrom@/components/tour/tour-steps.src/components/tour/onboarding-tour.tsx#L13-L21: import the store and step definitions through@/components/tour/*.src/components/tour/tour-help-button.tsx#L6-L6: import the store from@/components/tour/tour-store.
As per coding guidelines, src/**/*.{ts,tsx} requires the @/* path alias for imports.
📍 Affects 3 files
src/components/tour/tour-store.ts#L4-L4(this comment)src/components/tour/onboarding-tour.tsx#L13-L21src/components/tour/tour-help-button.tsx#L6-L6
🤖 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 `@src/components/tour/tour-store.ts` at line 4, Replace the relative tour
imports with `@/`* aliases throughout the affected files: update TourKind in
src/components/tour/tour-store.ts:4, the store and step-definition imports in
src/components/tour/onboarding-tour.tsx:13-21, and the store import in
src/components/tour/tour-help-button.tsx:6. Preserve the imported symbols and
behavior.
Source: Coding guidelines
| async function fetchVendorFeed(filters: VendorFeedFilters) { | ||
| const normalizedSearch = normalizeSearchTerm(filters.search); | ||
| const normalizedFilters = { ...filters, search: normalizedSearch }; | ||
|
|
||
| const supabase = normalizedSearch | ||
| ? createServerClient(await cookies()) | ||
| : createAdminClient(); | ||
|
|
||
| return getVendors({ ...normalizedFilters, supabaseProp: supabase, throwOnError: true }); | ||
| // Approved vendors + public category/owner data: safe to read with the | ||
| // service-role client. Errors propagate so they are not cached. | ||
| return getVendors({ | ||
| ...normalizedFilters, | ||
| supabaseProp: createAdminClient(), | ||
| throwOnError: true, | ||
| }); | ||
| } | ||
|
|
||
| /** Cached vendor feed (60s). */ | ||
| export const getCachedVendors = unstable_cache(fetchVendors, ['vendors-feed'], { | ||
| export const getCachedVendors = unstable_cache(fetchVendorFeed, ['vendors-feed'], { | ||
| revalidate: FEED_REVALIDATE, | ||
| }); |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate generated Supabase types or existing vendor result types.
fd -t f -e ts -e tsx . src | rg '(database|supabase|vendor).*\.tsx?$'
# Inspect the vendor query boundary before introducing the concrete result type.
ast-grep outline src/utils/supabase/queries/vendors.ts --items all
rg -nP --type=ts -C 4 'export\s+async\s+function\s+getVendors\b|\bgetVendors\s*\(' srcRepository: royal334/my-school-and-I
Length of output: 4952
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== cache/vendors.ts =="
cat -n src/utils/cache/vendors.ts
echo
echo "== vendors query boundary =="
sed -n '1,140p' src/utils/supabase/queries/vendors.ts | cat -n
echo
echo "== generated database types around vendor =="
python3 - <<'PY'
from pathlib import Path
p=Path('src/utils/supabase/database.types.ts')
if not p.exists():
print('missing database.types.ts')
else:
lines=p.read_text().splitlines()
for i,l in enumerate(lines,1):
if '"vendor" ::' in l or "interface Database" in l or "export type Database" in l or "vendor_" in l or "admin_roles" in l:
lo=max(1,i-40); hi=min(len(lines),i+80)
print(f'-- around line {i} --')
for j in range(lo,hi+1):
print(f'{j}: {lines[j-1]}')
break
PY
echo
echo "== all getVendors usages =="
rg -nP --type=ts -C 3 '\bgetVendors\s*\(' srcRepository: royal334/my-school-and-I
Length of output: 12714
Add explicit vendor result types for the feed and search APIs.
getVendors returns Promise<any> because the query uses supabaseProp?: any and cast filters inside filter/sort. fetchVendorFeed, getCachedVendors, and getVendorSearch all inherit that type at the exported boundary. Use the generated Database['public']['Tables']['vendors'] type plus the joined vendor_categories and profiles shape for getVendors, then type these cache/search APIs explicitly.
🤖 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 `@src/utils/cache/vendors.ts` around lines 34 - 50, Define a shared explicit
vendor result type using Database['public']['Tables']['vendors'] together with
the joined vendor_categories and profiles shapes, and apply it to getVendors’
return type. Replace the any-based query typing and casts with this result
shape, then explicitly type fetchVendorFeed, getCachedVendors, and
getVendorSearch so their exported APIs return the typed vendor results.
Source: Coding guidelines
| if (ids) { | ||
| query = query.in("id", ids); | ||
| } |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
curl -fsSL \
'https://raw.githubusercontent.com/supabase/supabase-js/v2.95.3/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts' |
sed -n '335,358p'Repository: royal334/my-school-and-I
Length of output: 885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
fd -a 'materials\.ts$|page\.tsx$|package.json$|postgrest|supabase' . | sed 's#^\./##' | head -120
echo
echo "== target file outline / relevant sections =="
target="$(fd -a 'materials\.ts$' . | head -1 | sed 's#^\./##')"
echo "target=$target"
wc -l "$target"
ast-grep outline "$target" --view compact || true
sed -n '1,120p' "$target"
echo
echo "== package versions =="
for f in package.json pnpm-lock.yaml yarn.lock package-lock.json; do
[ -f "$f" ] && { echo "--- $f"; rg -n '"(`@supabase/supabase-js`|`@supabase/postgrest-js`)"|(`@supabase/supabase-js`|`@supabase/postgrest-js`)@[0-9]' "$f" -m 20 || true; }
done
echo
echo "== imports/usages of getMaterials / materials query =="
rg -n "getMaterials|materials:" -S . --glob '*.ts' --glob '*.tsx' | head -100Repository: royal334/my-school-and-I
Length of output: 5710
🌐 Web query:
PostgREST in filter empty list in.() documentation
💡 Result:
In PostgREST, the in filter expects a non-empty list of values, for example: ?column=in.(value1,value2) [1][2]. The PostgREST API documentation does not define behavior for an empty list within the in operator, and using in.() in a URL generally results in an error or inconsistent behavior depending on the column type [3]. Historically, there have been discussions and GitHub issues regarding support for empty lists (e.g., representing an empty set), but PostgREST has not implemented this as a standard feature [3]. If you require filtering for an empty set or a scenario where no items should match, it is recommended to use alternative filtering logic, such as using not.is.null (if applicable) or structuring your query to exclude all records explicitly [4]. Be aware that () is a reserved character sequence in PostgREST URL syntax [5]. While empty parentheses are used for other purposes, such as resource embedding (e.g., ?select=*,table()) [6], they are not valid syntax for the in filter [3][6].
Citations:
- 1: https://postgrest.org/en/stable/references/api/tables_views.html
- 2: https://postgrest.org/en/v11/references/api/tables_views.html
- 3: Allow 'in.' filter to have no items PostgREST/postgrest#641
- 4: https://github.com/PostgREST/postgrest/blob/b8ca1bb0/test/spec/Feature/Query/QuerySpec.hs
- 5: https://docs.postgrest.org/en/stable/references/api/url_grammar.html
- 6: When embedding with top-level filtering (inner join), empty parentheses should be allowed PostgREST/postgrest#2340
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== package-lock supabase postgrest entries =="
python3 - <<'PY'
import json
path='package-lock.json'
data=json.load(open(path))
for pkg in ('`@supabase/supabase-js`','`@supabase/postgrest-js`'):
print(f'--- {pkg}')
print(data.get('packages',{}).get(f'node_modules/{pkg}',{}).get('version'))
for name, spec in data.get('packages',{}).items():
if spec.get('version') in ('2.95.3','2.109.0','2.109.0'):
if 'supabase-js' in name or 'postgrest-js' in name:
print(name, spec.get('version'))
PY
echo
echo "== fetch supabase-js v2.95.3 in() implementation =="
python3 - <<'PY'
from urllib.request import urlopen
url='https://raw.githubusercontent.com/Supabase/postgrest-js/v2.109.0/src/filters.ts'
try:
text=urlopen(url, timeout=20).read().decode()
except Exception as e:
print('fetch_filters_failed', e)
raise SystemExit
for i,line in enumerate(text.splitlines(),1):
if 'in(' in line or "name = 'in'" in line:
start=max(1,i-5); end=min(len(text.splitlines()),i+20)
print(f'--- {url} lines {start}-{end}')
for n,l in enumerate(text.splitlines()[start-1:end], start):
print(f'{n}: {l}')
PY
echo
echo "== fetch old supabase-js v2.95.3 in implementation if available =="
python3 - <<'PY'
from urllib.request import urlopen
for url in (
'https://raw.githubusercontent.com/supabase/supabase-js/v2.95.3/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts',
):
lines=urlopen(url, timeout=20).read().decode().splitlines()
for i,line in enumerate(lines[:], 1):
if line.strip().startswith('in<'):
start=max(1,i-3); end=min(len(lines),i+40)
print(f'--- {url} lines {start}-{end}')
for n,l in enumerate(lines[start-1:end], start):
print(f'{n}: {l}')
break
PYRepository: royal334/my-school-and-I
Length of output: 2927
🌐 Web query:
Supabase postgrest-js v2.109.0 filters.ts in values.map join comma
💡 Result:
In postgrest-js (including version 2.109.0), the logic for handling array values in filters like.in is implemented within the PostgrestFilterBuilder class [1][2]. When providing an array of values to the.in method, the library processes the input to ensure compatibility with PostgREST syntax [1][2]. Specifically, it performs the following steps: 1. Normalization: It creates a Set from the provided values to remove duplicates (Array.from(new Set(values))) [1][2]. 2. Sanitization: It maps over these values to handle PostgREST reserved characters (such as commas and parentheses) [1][2]. If a value is a string and contains characters matched by the reserved character regular expression ([,]), the library wraps that value in double quotes ("") [1][2]. 3. Joining: Finally, it joins the processed values using a comma (,) as the separator [1][2]. This resulting string is then formatted into the final query parameter as in.(values_joined_with_commas) [1][2]. This implementation ensures that special characters within the individual elements of the filter array do not break the PostgREST query structure [3].
Citations:
- 1: https://github.com/supabase/postgrest-js/blob/63c5d9a/src/PostgrestFilterBuilder.ts
- 2: https://cdn.jsdelivr.net/npm/@supabase/postgrest-js@2.110.8/src/PostgrestFilterBuilder.ts
- 3: fix: handle postgrest special characters in filters supabase/postgrest-js#166
🌐 Web query:
Supabase postgrest-js in empty array query URL ?column=in.()
💡 Result:
In Supabase's postgrest-js library, passing an empty array to the.in method produces a query URL with the parameter format?column=in. [1][2][3]. This resulting URL, which uses an empty set inside the parenthesis, is syntactically accepted by PostgREST [4]. When PostgREST processes a filter in the form of?column=in., it interprets this as an empty set and will generally return no matching rows, as no value can satisfy an "in" condition against an empty list [5]. It is important to note that the implementation of the.in method in postgrest-js ensures that values are cleaned and properly formatted before being appended to the URL [1][2][3]. While older versions or specific configurations may have encountered issues with empty or malformed inputs, modern versions of the library handle this by generating the empty parenthesis structure explicitly [1][2][3]. If you are experiencing unexpected behavior, ensure you are using a recent version of the library, as recent updates have specifically addressed edge cases regarding reserved character escaping and filter generation [6][7].
Citations:
- 1: https://github.com/supabase/postgrest-js/blob/master/src/PostgrestFilterBuilder.ts
- 2: https://github.com/supabase/postgrest-js/blob/63c5d9a/src/PostgrestFilterBuilder.ts
- 3: https://github.com/supabase/supabase-js/blob/21e9d4b7/packages/core/postgrest-js/src/PostgrestFilterBuilder.ts
- 4: https://postgrest.org/en/latest/references/api/tables%5Fviews.html
- 5: Allow 'in.' filter to have no items PostgREST/postgrest#641
- 6: supabase/postgrest-js@e7089f6
- 7: fix(postgrest): escape " and \ inside quoted filter values supabase/supabase-js#2529
🌐 Web query:
Site:github.com/postgrest/postgrest ?column=in.() empty list accepted
💡 Result:
In PostgREST, the query parameter syntax ?column=in.() is used to filter records where the value of column matches any value provided within the parentheses [1]. When the parentheses are empty, in.() represents an empty list. In SQL terms, this is functionally equivalent to column IN (), which typically results in a filter that matches no rows, as it is logically impossible for a value to be contained within an empty set. Regarding the technical behavior of this syntax: 1. Usage: The in operator is used to filter by a list of comma-separated values, such as ?column=in.(val1,val2) [1]. 2. Empty List: While the syntax in.() is valid in terms of the URL grammar processed by PostgREST [1], providing an empty list will effectively cause the query to return no results for that specific filter criterion. 3. Reserved Characters: Because parentheses and commas are reserved characters in the PostgREST URL grammar, they must be handled correctly [1]. If you are constructing URLs programmatically or including values that contain these characters, ensure they are properly percent-encoded (e.g., in.() or with double quotes for specific values) [1]. If you are experiencing unexpected behavior with in.(), ensure your HTTP client or library is not stripping or modifying the parentheses before the request reaches the server, as some tools may interpret them as special characters [1].
Citations:
🌐 Web query:
PostgREST in() empty list in.( returns 400 in. empty filter
💡 Result:
In PostgREST, using the in operator with an empty list—for example,?column=in.—is not natively supported and will typically result in a 400 Bad Request error [1]. PostgREST treats the input following the in operator as a list of values to match against [2][3]. When provided with an empty set of parentheses, the parser may fail to generate a valid SQL query or encounter inconsistencies depending on the column type [1]. Historically, this has been discussed as a limitation, and the API expects a non-empty list of values for the in operator [1]. If your application logic requires handling an empty set of filters, you may need to implement a workaround at the application or client level, such as: 1. Client-side logic: Check if the list of values is empty before constructing the URL parameter. If the list is empty, omit the filter entirely (or apply a different filter logic that reflects your requirements) instead of sending?column=in. [1][4]. 2. Alternative operators: If you are working with PostgreSQL arrays, ensure your queries are designed to handle empty array literals ('{}') if passed via RPCs or other custom endpoints, though this is distinct from the standard in filter syntax [5]. The 400 error is a standard response when PostgREST receives a malformed or unsupported filter structure [1]. Ensuring the filter string is either omitted or contains at least one valid value will prevent this error [1].
Citations:
- 1: Allow 'in.' filter to have no items PostgREST/postgrest#641
- 2: https://postgrest.org/en/stable/references/api/tables_views.html
- 3: https://postgrest.org/en/v11/references/api/tables_views.html
- 4: Empty list in
oroperator triggering 400 response sassoftware/postgrest-client#30 - 5: https://stackoverflow.com/questions/72712455/postgresql-rpcs-allow-required-array-parameters-that-will-be-processed-in-any
Handle an empty ids array before calling .in().
ids=[] is truthy, so this branch sends an empty in filter. That can depend on the deployed PostgREST version’s handling of empty lists. Return [] for an empty ID set, then apply .in() only to non-empty arrays.
Proposed fix
+ if (ids?.length === 0) {
+ return [];
+ }
+
- if (ids) {
+ if (ids?.length) {
query = query.in("id", ids);
}📝 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.
| if (ids) { | |
| query = query.in("id", ids); | |
| } | |
| if (ids?.length === 0) { | |
| return []; | |
| } | |
| if (ids?.length) { | |
| query = query.in("id", ids); | |
| } |
🤖 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 `@src/utils/supabase/queries/materials.ts` around lines 59 - 61, Update the ids
handling around the query builder so an empty ids array immediately returns an
empty result, while the existing query.in("id", ids) call runs only for
non-empty arrays. Preserve the current behavior when ids is absent or contains
values.
Source: MCP tools
Summary by CodeRabbit