Skip to content

React joyride addition - #12

Merged
royal334 merged 4 commits into
mainfrom
react-joyride-addition
Aug 7, 2026
Merged

React joyride addition#12
royal334 merged 4 commits into
mainfrom
react-joyride-addition

Conversation

@royal334

@royal334 royal334 commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added guided onboarding tours for student and vendor dashboards, with desktop/mobile support and replay controls.
    • Added tour help access from dashboard navigation.
    • Added upgrade prompts for basic-tier vendors viewing conversion analytics.
  • Bug Fixes
    • Improved materials filtering and prevented unnecessary initial filter updates.
    • Improved vendor search freshness and reliability.
    • Added clearer empty results handling when no saved materials exist.
  • Usability
    • Added tour guidance across dashboard pages, navigation, and key actions.

@vercel

vercel Bot commented Aug 7, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
my-school-and-i Ready Ready Preview Aug 7, 2026 12:10pm
my-school-and-i-9enh Ready Ready Preview Aug 7, 2026 12:10pm
my-school-and-i-h4cj Ready Ready Preview Aug 7, 2026 12:10pm
my-school-and-i-t7lp Ready Ready Preview Aug 7, 2026 12:10pm

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Dashboard onboarding tour

Layer / File(s) Summary
Tour steps and state
src/components/tour/tour-steps.ts, src/components/tour/tour-store.ts, package.json
Adds responsive student and vendor tour steps, target resolution, and Zustand tour state. Reorders related dependencies.
Tour runtime and controls
src/components/tour/onboarding-tour.tsx, src/components/tour/tour-help-button.tsx, src/components/dashboard/mobile-header-menu.tsx
Adds automatic and forced tour starts, route synchronization, Joyride event handling, completion persistence, diagnostics, and replay controls.
Dashboard wiring and targets
src/app/dashboard/layout.tsx, src/app/dashboard/*/page.tsx, src/components/dashboard/*, src/components/materials/materials-content.tsx, src/components/vendors/vendor-*.tsx
Wires the tour into the dashboard and adds data-tour targets across desktop, mobile, student, vendor, and page content.

Materials and vendor data flows

Layer / File(s) Summary
Materials filtering and retrieval
src/app/dashboard/materials/page.tsx, src/components/materials/materials-filters.tsx, src/utils/supabase/queries/materials.ts
Uses the admin client for saved-material retrieval, handles empty saved IDs, and skips initial filter updates while preserving delayed updates.
Vendor search and analytics access
src/utils/cache/vendors.ts, src/app/dashboard/vendors/page.tsx, src/components/vendors/analytics-dashboard.tsx
Adds uncached authenticated vendor search, keeps cached vendor feeds on the service-role client, and shows an upgrade panel to basic-tier vendors.

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
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 13.51% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title accurately identifies the main change: adding React Joyride-based onboarding tours.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch react-joyride-addition

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.

❤️ Share

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

@royal334
royal334 merged commit 3e7f5ba into main Aug 7, 2026
5 of 6 checks passed

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Propagate errors from the material_saves query.

When showSavedOnly is true and the saved-material query fails, the code treats data as 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

📥 Commits

Reviewing files that changed from the base of the PR and between eee7f2b and 2ac9be6.

⛔ Files ignored due to path filters (1)
  • dev-server.log is excluded by !**/*.log
📒 Files selected for processing (29)
  • package.json
  • src/app/dashboard/announcements/page.tsx
  • src/app/dashboard/cgpa/page.tsx
  • src/app/dashboard/layout.tsx
  • src/app/dashboard/materials/page.tsx
  • src/app/dashboard/notifications/page.tsx
  • src/app/dashboard/profile/page.tsx
  • src/app/dashboard/settings/page.tsx
  • src/app/dashboard/subscription/page.tsx
  • src/app/dashboard/vendors/analytics/page.tsx
  • src/app/dashboard/vendors/page.tsx
  • src/components/dashboard/DashboardQuickActions.tsx
  • src/components/dashboard/DashboardQuickStats.tsx
  • src/components/dashboard/DashboardWelcomeHeader.tsx
  • src/components/dashboard/dashboard-toggle.tsx
  • src/components/dashboard/mobile-bottom-nav.tsx
  • src/components/dashboard/mobile-header-menu.tsx
  • src/components/dashboard/vendor-dashboard.tsx
  • src/components/materials/materials-content.tsx
  • src/components/materials/materials-filters.tsx
  • src/components/tour/onboarding-tour.tsx
  • src/components/tour/tour-help-button.tsx
  • src/components/tour/tour-steps.ts
  • src/components/tour/tour-store.ts
  • src/components/vendors/analytics-dashboard.tsx
  • src/components/vendors/vendor-mobile-bottom-nav.tsx
  • src/components/vendors/vendor-sidebar.tsx
  • src/utils/cache/vendors.ts
  • src/utils/supabase/queries/materials.ts

Comment on lines +25 to +31
useEffect(() => {
const isStudent = document.cookie
.split('; ')
.find((r) => r.startsWith('isStudent='))
?.split('=')[1] !== 'false';
isVendorViewRef.current = isVendorAccount || (hasVendor && !isStudent);
}, [hasVendor, isVendorAccount]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

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.

Comment on lines +87 to +94
// Debounced search
const isFirstRender = useRef(true);

useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
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 -200

Repository: 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

Comment on lines +96 to +105
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
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
fi

Repository: 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

Comment on lines +52 to +55
function useIsMobile() {
const [isMobile, setIsMobile] = useState(() =>
typeof window !== 'undefined' && window.matchMedia('(max-width: 767.98px)').matches,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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' || true

Repository: 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"; }
done

Repository: 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.,

inside a

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:


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

Comment on lines +120 to +123
const forceStart =
typeof window !== 'undefined' &&
new URLSearchParams(window.location.search).has('tourStart');
if (hasSeenTour(kind, userId) && !forceStart) return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

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.

Comment on lines +244 to +259
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);
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

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 || true

Repository: 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 || true

Repository: 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:


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.

Comment on lines +126 to +130
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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep every referenced page target mounted.

  • src/components/tour/tour-steps.ts#L126-L130: retain the materials step only if page-materials exists in every materials state.
  • src/components/tour/tour-steps.ts#L147-L151: retain the vendors step only if page-vendors exists 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: move data-tour="page-materials" to a wrapper that also contains the empty state.
  • src/app/dashboard/vendors/page.tsx#L110-L110: move data-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-L151
  • src/components/tour/tour-steps.ts#L259-L264
  • src/components/tour/tour-steps.ts#L280-L285
  • src/components/materials/materials-content.tsx#L97-L97
  • src/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';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use @/* imports throughout the tour feature.

  • src/components/tour/tour-store.ts#L4-L4: import TourKind from @/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-L21
  • src/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

Comment on lines +34 to 50
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,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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*\(' src

Repository: 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*\(' src

Repository: 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

Comment on lines +59 to 61
if (ids) {
query = query.in("id", ids);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
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 -100

Repository: 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:


🏁 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
PY

Repository: 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:


🌐 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:


🌐 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:


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.

Suggested change
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

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant