Skip to content

Conversation

@mohanish28
Copy link

@mohanish28 mohanish28 commented Sep 4, 2025

added analytics feature
Fixes #107

Summary by CodeRabbit

  • New Features

    • Added full analytics (retention, review-intervals, session heatmap, subject progress, time-spent, difficulty insights, recommendations) and related UI components/pages
    • Added payments model and Razorpay order creation; pricing page and analytics links
  • Bug Fixes

    • Improved error handling and logging in payment and API flows
    • Better HTTP error reporting on generator/API responses
  • Chores

    • Sanitized environment sample and updated .gitignore
    • Docker OpenSSL compatibility, added Chart.js deps, Prisma indexes and generator targets, build config tweaks

@vercel
Copy link
Contributor

vercel bot commented Sep 4, 2025

@mohanish28 is attempting to deploy a commit to the devsuraj Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link

coderabbitai bot commented Sep 4, 2025

Walkthrough

Adds an analytics feature set (SSR dashboard, client chart components, new analytics API routes), payment/subscription schema and payment APIs refactor, Chart.js deps, Docker OpenSSL compatibility tweak, sanitized .env.example and .gitignore, UI navigation tweaks, and several pages marked dynamic = 'force-dynamic'.

Changes

Cohort / File(s) Summary
Env & gitignore
/.env.example, /.gitignore
Reorganized and sanitized .env.example (removed sample values; grouped Clerk, Razorpay, Database, Gemini API placeholders); added /.clerk/ to .gitignore.
Container config
/Dockerfile
Switched opensslopenssl1.1-compat in both build and runtime stages; updated comments.
Dependencies
/package.json
Added chart.js (^4.5.0) and react-chartjs-2 (^5.3.0).
Prisma schema
/prisma/schema.prisma
Added Payment model, PaymentStatus enum, new user subscription/payment fields and relations, multiple indexes, and binaryTargets in generator.
Analytics API routes
/src/app/api/analytics/...
/subject-progress/route.ts, /retention-curve/route.ts, /review-interval-effectiveness/route.ts, /session-heatmap/route.ts, /time-spent/route.ts
New authenticated GET endpoints validating query params, querying Prisma for user-specific analytics, aggregating/transforming results, and returning JSON with appropriate error handling and cache headers.
Analytics dashboard page
/src/app/(dashboard)/analytics/page.tsx
New server component analytics page (exports dynamic = 'force-dynamic'), requires auth, builds query string helper, fetches analytics data, and renders Filters + chart wrappers with Suspense fallbacks.
Analytics components & wrappers
/src/components/analytics/*
Added client chart components and thin wrappers: SubjectProgress, RetentionCurve, SessionHeatmap, TimeSpentAnalysis, ReviewIntervalEffectiveness, DifficultyInsights, Recommendations, and wrapper components for SubjectProgress/RetentionCurve/SessionHeatmap.
Filters UI
/src/components/analytics/Filters.tsx
New client filter bar that syncs subject/dateRange/difficulty/timezone to URL search params and navigates on change; auto-detects timezone on mount.
Timezone utils
/src/lib/utils/timezone.ts
Added timezone helpers: formatDateInTimezone, getDayInTimezone, getHourInTimezone, isValidTimezone (uses date-fns-tz and Intl validation).
Navigation & core UI
/src/app/flashcards/page.tsx, src/components/core/flash-card.tsx
Added “Analytics” navigation buttons/links; flashcards page uses client-side deck fetching and loading states; core flash-card replaced single action with two-button layout including Analytics.
Flashcard generator error handling
/src/components/flashcards/FlashcardGenerator.tsx
Improved non-OK response handling: parses JSON body and includes server error field in thrown error messages.
Payment integrations refactor
/src/app/api/checkout-sessions/route.ts, src/app/api/razorpay/create-order/route.ts
Replaced global SDK instances with per-request helpers (getStripeInstance, getRazorpayInstance) that validate env vars and return configured clients; added error logging/validation.
Layout & toasts
/src/app/layout.tsx
Removed react-toastify ToastContainer usage, kept Toaster, added runtime check/fallback for NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY, exported dynamic = 'force-dynamic'.
Dynamic flags (pages)
/src/app/(dashboard)/generate/page.tsx, /src/app/flashcards/[id]/page.tsx, /src/app/pricing/page.tsx, /src/app/result/page.tsx
Added export const dynamic = 'force-dynamic' to multiple pages to force dynamic SSR.
Not-found page
/src/app/not-found.tsx
Added client-side 404 with Go Home and Go Back actions.
Next config
/next.config.js
Commented out output line; added trailingSlash: false and generateEtags: false with explanatory comments.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor U as User
  participant AP as Analytics Page (SSR)
  participant F as Filters (Client)
  participant C as Chart Component (Client)
  participant API as Analytics API Route
  participant DB as Prisma

  U->>AP: GET /analytics
  AP->>U: HTML (Filters + chart placeholders)
  U->>F: change filters
  F->>U: update URL (search params)
  C->>API: GET /api/analytics/<endpoint>?subject&dateRange&difficulty&timezone
  API->>DB: query study data for user with filters
  DB-->>API: records
  API-->>C: JSON { labels, data... }
  C-->>U: render chart(s)
Loading
sequenceDiagram
  autonumber
  actor U as User
  participant FG as FlashcardGenerator (Client)
  participant S as Server API (generate)
  participant Err as Error path

  U->>FG: submit prompt
  FG->>S: POST /api/generate
  S--xFG: 4xx/5xx (non-OK)
  FG->>S: parse JSON body
  S-->>FG: { error: "message" }
  FG->>Err: throw Error("message")
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feature project enhancement #84 — Overlapping changes affecting env variables (GEMINI/Razorpay) and Prisma payment/subscription schema; likely related to payment/env reorganization.

Suggested labels

enhancement

Poem

I hop through logs and charts tonight, 🥕
I stitch new graphs by soft moonlight.
Heatmaps hum and curves take flight,
Payments and envs tucked in tight.
Hooray — insights bloom in burrowed bytes! 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Consider removing or moving to a separate PR the unrelated changes: src/app/not-found.tsx (new 404 page), .env.example restructuring (can be a documentation-only change), and infrastructure configuration changes (Dockerfile, next.config.js, multiple dynamic='force-dynamic' exports across unrelated pages). Keep this PR focused on the analytics implementation: new analytics page, components, API routes, database schema updates, and UI navigation links to analytics. Infrastructure fixes causing build failures should be addressed in separate PRs with clear justification or as prerequisite fixes.
Docstring Coverage ⚠️ Warning Docstring coverage is 42.11% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The pull request title "added analytics feature Fixes #107" clearly and concisely communicates the main purpose of the changeset. It directly describes the primary change—adding analytics functionality—and references the linked issue. The title is specific enough that a reviewer scanning the commit history would immediately understand this PR introduces new analytics capabilities. While the phrasing could be slightly more polished (e.g., "Add analytics features" instead of "added analytics feature"), it sufficiently conveys the core contribution without vagueness or noise.
Linked Issues Check ✅ Passed The pull request successfully implements all primary objectives from linked issue #107. Per-subject progress is provided via the subject-progress API route and SubjectProgress component; retention curves are delivered through the retention-curve route and RetentionCurve component; session heatmaps are implemented with the session-heatmap route and SessionHeatmap component; and retention trend visualization is supported by the retention curve and review interval effectiveness analyses. The Prisma schema updates with new indexes on StudySession, StudyRecord, and Deck support efficient analytics queries. The new analytics page serves as the hub, fetching data from these routes and rendering all visualization components with proper filtering and timezone support.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

Caution

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

⚠️ Outside diff range comments (2)
prisma/schema.prisma (2)

116-118: Define referential action for Payment→User (deletes/updates).

Without onDelete/onUpdate, user deletes may fail or strand rows.

-  user                User      @relation(fields: [userId], references: [clerkUserId])
+  user                User      @relation(fields: [userId], references: [clerkUserId], onDelete: Cascade, onUpdate: NoAction)

If you don’t want cascades, prefer onDelete: SetNull with userId String?.


31-38: Add mandatory backfill migration for clerkUserId
Create a one-time migration (e.g. in prisma/migrations/) that populates the userId column on Deck, Flashcard, StudySession and Payment tables from the existing User.clerkUserId, so legacy rows correctly reference the new key.

🧹 Nitpick comments (39)
package.json (2)

36-36: Chart.js added — watch for SSR and bundle size.

Chart.js touches window and isn’t SSR-friendly out of the box. In Next.js, render chart components client-side only to avoid hydration issues and keep bundles lean.

Suggestions:

  • Wrap chart components in a client-only wrapper and/or use next/dynamic with ssr: false.
  • Centralize Chart.register(...) in a single module for tree-shaking-friendly registration (e.g., LineElement, BarElement, CategoryScale, LinearScale, Tooltip, Legend).
  • If analytics pages are route-split, lazy-load charts to reduce initial payload.

49-49: react-chartjs-2 peer/SSR considerations.

Ensure the installed version of react-chartjs-2 matches chart.js v4 and React 19. Prefer a thin wrapper component that registers chart.js once and is dynamically imported with ssr: false.

If helpful, I can add a shared ChartSetup.ts with proper registrations and an example dynamic import.

src/components/flashcards/FlashcardGenerator.tsx (1)

54-56: Harden error parsing to avoid “undefined” messages.

If the error body isn’t JSON or lacks error, this can throw or display “undefined”. Fallback to statusText/text.

Apply:

-      if (!response.ok) {
-        const errorData = await response.json();
-        throw new Error(`HTTP error! status: ${response.status}, message: ${errorData.error}`);
-      }
+      if (!response.ok) {
+        let message = response.statusText || 'Request failed';
+        try {
+          const errorData = await response.json();
+          message = errorData?.error || message;
+        } catch (_) {
+          try { message = await response.text(); } catch (_) {}
+        }
+        throw new Error(`Request failed ${response.status}: ${message}`);
+      }

Optional: surface error.message in the toast description for better UX.

Dockerfile (2)

6-7: Builder stage: unnecessary packages.

postgresql-client isn’t needed to build; drop it here to slim the image.

-RUN apk add --no-cache openssl1.1-compat libc6-compat postgresql-client
+RUN apk add --no-cache openssl1.1-compat libc6-compat

Additionally, confirm whether openssl1.1-compat is actually required by any native deps. If Prisma engines target linux-musl with OpenSSL 3, prefer libssl3 instead.

Run-time check: verify prisma/schema.prisma binaryTargets include linux-musl or rely on native built in Alpine.


61-63: Runtime OpenSSL choice may be mismatched.

If your Prisma engines are built against OpenSSL 3 on Alpine (linux-musl-openssl-3.0.x), openssl1.1-compat is unnecessary. Keep postgresql-client here for pg_isready, but verify which libssl you need.

Suggestions:

  • If engines are OpenSSL 3: use libssl3 instead of openssl1.1-compat.
  • Consider checking DB readiness with DATABASE_URL (pg_isready -d "$DATABASE_URL") to avoid hardcoding host/user in CMD.

I can propose an updated CMD using DATABASE_URL if desired.

src/components/analytics/SubjectProgress.tsx (1)

25-46: Make dataset configurable (prep for real data)

Accept labels/datasets via props so this component can render API-driven data.

-export function SubjectProgress() {
-  const data = {
-    labels: ['Week 1', 'Week 2', 'Week 3', 'Week 4'],
-    datasets: [
-      {
-        label: 'React',
-        data: [65, 75, 80, 85],
-        fill: false,
-        borderColor: 'rgb(75, 192, 192)',
-        tension: 0.1,
-      },
-      {
-        label: 'Next.js',
-        data: [70, 72, 78, 82],
-        fill: false,
-        borderColor: 'rgb(255, 99, 132)',
-        tension: 0.1,
-      },
-    ],
-  };
-
-  return <Line data={data} />;
+type SubjectProgressProps = {
+  labels: string[];
+  datasets: { label: string; data: number[]; borderColor?: string; fill?: boolean; tension?: number }[];
+};
+
+export function SubjectProgress({ labels, datasets }: SubjectProgressProps) {
+  return <Line data={{ labels, datasets }} />;
 }
src/components/analytics/Filters.tsx (4)

12-16: Avoid pushing a new URL on every keystroke

Debounce updates or trigger onBlur to prevent router history spam and unnecessary re-renders.

-  const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+  const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     const params = new URLSearchParams(searchParams.toString());
     params.set(event.target.name, event.target.value);
-    router.push(`?${params.toString()}`);
+    // TODO: debounce 300ms or call onBlur instead of onChange for text inputs
+    router.push(`?${params.toString()}`);
   };

21-31: Inputs don’t reflect current query values

Initialize with existing search params so UI mirrors active filters.

-        <Input id="subject" name="subject" onChange={handleFilterChange} />
+        <Input id="subject" name="subject" defaultValue={searchParams.get('subject') ?? ''} onChange={handleFilterChange} />
...
-        <Input id="dateRange" name="dateRange" type="date" onChange={handleFilterChange} />
+        <Input id="dateRange" name="dateRange" type="date" defaultValue={searchParams.get('dateRange') ?? ''} onChange={handleFilterChange} />
...
-        <Input id="difficulty" name="difficulty" onChange={handleFilterChange} />
+        <Input id="difficulty" name="difficulty" defaultValue={searchParams.get('difficulty') ?? ''} onChange={handleFilterChange} />

29-31: Constrain difficulty to known values

Use a Select with EASY/MEDIUM/HARD to match backend expectations and avoid junk inputs.

If using shadcn/ui Select:

+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
...
-        <Input id="difficulty" name="difficulty" defaultValue={searchParams.get('difficulty') ?? ''} onChange={handleFilterChange} />
+        <Select
+          name="difficulty"
+          onValueChange={(value) => {
+            const params = new URLSearchParams(searchParams.toString());
+            params.set('difficulty', value);
+            router.push(`?${params.toString()}`);
+          }}
+          defaultValue={searchParams.get('difficulty') ?? ''}
+        >
+          <Label htmlFor="difficulty">Difficulty</Label>
+          <SelectTrigger id="difficulty" className="w-[180px]"><SelectValue placeholder="Select" /></SelectTrigger>
+          <SelectContent>
+            <SelectItem value="EASY">EASY</SelectItem>
+            <SelectItem value="MEDIUM">MEDIUM</SelectItem>
+            <SelectItem value="HARD">HARD</SelectItem>
+          </SelectContent>
+        </Select>

25-26: “dateRange” is a single date

Name suggests a range; consider start/end to filter inclusively and avoid TZ edge cases.

src/components/analytics/RetentionCurve.tsx (1)

25-39: Hook up to the retention API and filters

Replace static data with API response and current query params so the chart updates with Filters.

High-level approach:

  • Read search params (subject, dateRange, difficulty).
  • fetch(/api/analytics/retention-curve?${params}) in useEffect.
  • Map response into Line data.
src/app/api/analytics/time-spent/route.ts (2)

33-41: Guard against null/undefined record.timeSpent and clarify units

If timeSpent can be null, reduce will yield NaN. Also document/normalize units (seconds vs minutes).

-      const averageTimePerCard = totalCards > 0 ? session.records.reduce((acc, record) => acc + record.timeSpent, 0) / totalCards : 0; // Average in seconds
+      const totalTime = session.records.reduce((acc, record) => acc + (record.timeSpent ?? 0), 0);
+      const averageTimePerCard = totalCards > 0 ? totalTime / totalCards : 0; // seconds (normalize if needed)

19-20: Timezone/date inclusivity for dateRange

new Date('YYYY-MM-DD') parses as UTC; consider start/end of day window to avoid off-by-one in local time.

src/app/api/analytics/retention-curve/route.ts (2)

7-10: Prefer NextResponse.json for consistency and correctness.

Use the helper to avoid manual stringify and ensure correct headers.

-  if (!user) {
-    return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
-  }
+  if (!user) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }

29-43: Bucket by stable UTC day and keep labels/data aligned deterministically.

toLocaleDateString() varies by server locale/timezone. Use ISO date (UTC) to avoid off-by-one day grouping. Also derive retention from a sorted labels array to guarantee alignment.

-const retentionData = studyRecords.reduce((acc, record) => {
-  const date = new Date(record.createdAt).toLocaleDateString();
-  if (!acc[date]) {
-    acc[date] = { correct: 0, total: 0 };
-  }
-  acc[date].total++;
-  if (record.isCorrect) {
-    acc[date].correct++;
-  }
-  return acc;
-}, {} as { [key: string]: { correct: number; total: number } });
-
-const labels = Object.keys(retentionData);
-const retention = Object.values(retentionData).map((d) => (d.correct / d.total) * 100);
+const retentionData = new Map<string, { correct: number; total: number }>();
+for (const record of studyRecords) {
+  const dateKey = new Date(record.createdAt).toISOString().slice(0, 10); // YYYY-MM-DD (UTC)
+  const prev = retentionData.get(dateKey) ?? { correct: 0, total: 0 };
+  prev.total += 1;
+  if (record.isCorrect) prev.correct += 1;
+  retentionData.set(dateKey, prev);
+}
+const labels = Array.from(retentionData.keys()).sort();
+const retention = labels.map((k) => {
+  const d = retentionData.get(k)!;
+  return (d.correct / d.total) * 100;
+});
.env.example (2)

1-3: Order sensitive keys to satisfy dotenv linters.

Place the server secret before the public key as suggested.

-# Clerk
-NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=
-CLERK_SECRET_KEY=
+# Clerk
+CLERK_SECRET_KEY=
+NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=

10-17: Optional: add minimal examples or format hints.

Sample values reduce setup friction, e.g., DATABASE_URL="postgresql://user:pass@localhost:5432/db?schema=public". Also note that secrets must not be prefixed with NEXT_PUBLIC_.

src/components/analytics/TimeSpentAnalysis.tsx (2)

38-55: Abort in-flight requests and clear stale error; also opt out of caching.

Prevents race conditions on rapid filter changes and avoids stale responses.

-  useEffect(() => {
-    const fetchData = async () => {
+  useEffect(() => {
+    const controller = new AbortController();
+    setError(null);
+    const fetchData = async () => {
       try {
         const params = new URLSearchParams(searchParams.toString());
-        const response = await fetch(`/api/analytics/time-spent?${params.toString()}`);
+        const response = await fetch(
+          `/api/analytics/time-spent?${params.toString()}`,
+          { signal: controller.signal, cache: 'no-store' }
+        );
         if (!response.ok) {
-          throw new Error('Failed to fetch data');
+          const body = await response.json().catch(() => ({}));
+          throw new Error(body.error || 'Failed to fetch data');
         }
         const result = await response.json();
         setData(result);
       } catch (err) {
-        setError(err instanceof Error ? err.message : 'An unknown error occurred');
+        if ((err as any)?.name !== 'AbortError') {
+          setError(err instanceof Error ? err.message : 'An unknown error occurred');
+        }
       }
     };
 
     fetchData();
-  }, [searchParams]);
+    return () => controller.abort();
+  }, [searchParams]);

64-82: Optional: normalize dates to a fixed timezone for label stability.

If the API returns ISO dates, prefer new Date(d.date + 'T00:00:00Z') or render server-provided labels to avoid client TZ drift.

src/components/analytics/Recommendations.tsx (1)

55-61: Show an empty state when no urgent decks.

Avoid rendering an empty list.

-          <ul className="list-disc list-inside">
-            {data.urgentRevisionDecks.map(deck => (
-              <li key={deck.name}>{deck.name} ({deck.retention.toFixed(2)}% retention)</li>
-            ))}
-          </ul>
+          {data.urgentRevisionDecks.length === 0 ? (
+            <p className="text-sm text-gray-400">No urgent revisions right now.</p>
+          ) : (
+            <ul className="list-disc list-inside">
+              {data.urgentRevisionDecks.map(deck => (
+                <li key={deck.name}>{deck.name} ({deck.retention.toFixed(2)}% retention)</li>
+              ))}
+            </ul>
+          )}
src/components/analytics/DifficultyInsights.tsx (2)

36-56: Abort in-flight requests and clear stale error; also opt out of caching.

Same pattern as other analytics components.

-  useEffect(() => {
-    const fetchData = async () => {
-      setError(null);
+  useEffect(() => {
+    const controller = new AbortController();
+    const fetchData = async () => {
+      setError(null);
       try {
         const params = new URLSearchParams(searchParams.toString());
-        const response = await fetch(`/api/analytics/difficulty-insights?${params.toString()}`);
+        const response = await fetch(
+          `/api/analytics/difficulty-insights?${params.toString()}`,
+          { signal: controller.signal, cache: 'no-store' }
+        );
         
         if (!response.ok) {
           const errorData = await response.json();
           throw new Error(errorData.details || 'Failed to fetch data');
         }
 
         const result = await response.json();
         setData(result);
       } catch (err) {
-        setError(err instanceof Error ? err.message : 'An unknown error occurred');
+        if ((err as any)?.name !== 'AbortError') {
+          setError(err instanceof Error ? err.message : 'An unknown error occurred');
+        }
       }
     };
 
     fetchData();
-  }, [searchParams]);
+    return () => controller.abort();
+  }, [searchParams]);

97-109: Type tooltip callback to drop any and avoid implicit any bugs.

Improves safety and editor help.

-import {
+import {
   Chart as ChartJS,
   CategoryScale,
   LinearScale,
   BarElement,
   Title,
   Tooltip,
   Legend,
 } from 'chart.js';
+import type { ChartOptions, TooltipItem } from 'chart.js';
@@
-  const chartOptions = {
+  const chartOptions: ChartOptions<'bar'> = {
@@
-          label: function (context: any) {
+          label: function (context: TooltipItem<'bar'>) {
             let label = context.dataset.label || '';
             if (label) {
               label += ': ';
             }
-            const total = context.chart.data.datasets.reduce((acc: number, dataset: any) => acc + dataset.data[context.dataIndex], 0);
-            const value = context.raw;
+            const total = context.chart.data.datasets.reduce((acc: number, dataset) => {
+              const v = (dataset.data as number[])[context.dataIndex] ?? 0;
+              return acc + (typeof v === 'number' ? v : 0);
+            }, 0);
+            const value = Number(context.raw);
             const percentage = total > 0 ? ((value / total) * 100).toFixed(2) + '%' : '0%';
             return `${label}${value} (${percentage})`;
           }
prisma/schema.prisma (3)

20-27: Constrain subscription fields using enums instead of free-form strings.

Prevents data drift and simplifies validation.

Example:

-  subscriptionPlan     String?   @default("free") // "free", "basic", "pro"
-  subscriptionCycle    String?   // "monthly", "yearly"
-  subscriptionStatus   String?   @default("inactive") // "active", "inactive", "cancelled", "expired"
+  subscriptionPlan     SubscriptionPlan?   @default(free)
+  subscriptionCycle    SubscriptionCycle?
+  subscriptionStatus   SubscriptionStatus? @default(inactive)

Add enums at end of file:

+enum SubscriptionPlan { free basic pro }
+enum SubscriptionCycle { monthly yearly }
+enum SubscriptionStatus { active inactive cancelled expired }

101-107: Validate positive amounts.

Guard against zero/negative amounts at app layer or add a DB CHECK in a migration.

Migration snippet:

ALTER TABLE "Payment"
ADD CONSTRAINT "Payment_amount_positive" CHECK ("amount" > 0);

119-123: Consider composite index for common queries.

If you query Payments by user and recency, add a composite index to avoid bitmap index-and.

   @@index([userId])
   @@index([razorpayOrderId])
   @@index([status])
   @@index([createdAt])
+  @@index([userId, createdAt])
src/components/analytics/SessionHeatmap.tsx (2)

14-21: Avoid duplicate ChartJS.register calls across components.

Centralize registration in a single chart-setup module to keep bundles lean and prevent accidental double-registration.

Example: create src/lib/chart.ts to register once and import it where needed.


35-35: Add basic options for responsiveness and theming.

Improve UX on small screens and dark mode.

-  return <Bar data={data} />;
+  return <Bar data={data} options={{ responsive: true, maintainAspectRatio: false }} />;
src/app/(dashboard)/analytics/page.tsx (1)

26-42: Suspense likely ineffective with statically imported client components.

Unless these are dynamically imported or data-fetching with suspense, the fallback won’t show. Either remove Suspense or switch to next/dynamic with loading.

-import { RetentionCurve } from "@/components/analytics/RetentionCurve";
-import { SessionHeatmap } from "@/components/analytics/SessionHeatmap";
-import { SubjectProgress } from "@/components/analytics/SubjectProgress";
+import dynamic from "next/dynamic";
+const SubjectProgress = dynamic(() => import("@/components/analytics/SubjectProgress"), { ssr: false, loading: () => <LoadingSpinner /> });
+const RetentionCurve = dynamic(() => import("@/components/analytics/RetentionCurve"), { ssr: false, loading: () => <LoadingSpinner /> });
+const SessionHeatmap = dynamic(() => import("@/components/analytics/SessionHeatmap"), { ssr: false, loading: () => <LoadingSpinner /> });

Then you can drop the nested Suspense wrappers.

src/app/api/analytics/review-interval-effectiveness/route.ts (3)

11-14: Validate dateRange before querying.

Guard against invalid dates to avoid Prisma errors.

-  const dateRange = searchParams.get('dateRange');
+  const dateRange = searchParams.get('dateRange');
+  const from = dateRange ? new Date(dateRange) : undefined;
+  if (dateRange && isNaN(from!.getTime())) {
+    return NextResponse.json({ error: 'Invalid dateRange' }, { status: 400 });
+  }
...
-        ...(dateRange && { createdAt: { gte: new Date(dateRange) } }),
+        ...(from && { createdAt: { gte: from } }),

Also applies to: 22-23


40-55: Ensure per-flashcard ordering before diffing.

Global orderBy doesn’t guarantee order within each grouped array after reduce. Sort each group to be safe.

-    const reviewIntervals = Object.values(recordsByFlashcard).flatMap(records => {
+    const reviewIntervals = Object.values(recordsByFlashcard).flatMap(records => {
+        records.sort((a, b) => +new Date(a.createdAt) - +new Date(b.createdAt));
         if (records.length < 2) {
             return [];
         }
         return records.slice(1).map((record, index) => {
             const previousRecord = records[index];
             const interval = (new Date(record.createdAt).getTime() - new Date(previousRecord.createdAt).getTime()) / (1000 * 3600 * 24);
             return {
                 interval,
                 isCorrect: record.isCorrect,
             };
         });
     });

59-60: Add minimal context to logs or use a namespaced logger.

Keeps logs actionable in prod without leaking PII.

-    console.error('[REVIEW_INTERVAL_API]', error);
+    console.error('[REVIEW_INTERVAL_API] failed to compute intervals:', error);
src/app/api/analytics/session-heatmap/route.ts (5)

7-10: Use auth() and NextResponse.json for consistency and lighter auth.

Align with other routes and avoid fetching the full user object. Also simplify JSON responses.

-import { currentUser } from "@clerk/nextjs/server";
+import { auth } from "@clerk/nextjs/server";

 export async function GET(req: NextRequest) {
-  const user = await currentUser();
-  if (!user) {
-    return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
-  }
+  const { userId } = auth();
+  if (!userId) {
+    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
+  }

20-28: Make subject matching case-insensitive.

Users may pass “math” vs “Math”; use Prisma’s mode: 'insensitive'.

-              flashcard: {
-                deck: { name: subject },
-              },
+              flashcard: {
+                deck: { name: { equals: subject, mode: "insensitive" } },
+              },

35-41: Compute day/hour once and prefer UTC (or accept a tz param).

Avoid double Date construction and server TZ skew. If UX expects local times, consider a tz query param and mapping; otherwise use UTC consistently.

-  studySessions.forEach((session) => {
-    const day = new Date(session.startTime).getDay();
-    const hour = new Date(session.startTime).getHours();
-    if (heatmapData[day]) {
-      heatmapData[day][hour]++;
-    }
-  });
+  studySessions.forEach((session) => {
+    const dt = new Date(session.startTime);
+    const day = dt.getUTCDay();
+    const hour = dt.getUTCHours();
+    heatmapData[day][hour] += 1;
+  });

43-46: Prefer NextResponse.json; it sets headers for you.

Cleaner and less error-prone.

-  return new NextResponse(JSON.stringify(heatmapData), {
-    status: 200,
-    headers: { "Content-Type": "application/json" },
-  });
+  return NextResponse.json(heatmapData, { status: 200 });

16-19: Consider DB indexes for query shape.

Add indexes on StudySession(userId, startTime) and on the join path used for subject filtering to keep analytics snappy at scale.

src/components/analytics/ReviewIntervalEffectiveness.tsx (3)

27-43: Abort in-flight fetches and tighten dependencies.

Prevents setState on unmounted and avoids redundant requests.

-  useEffect(() => {
-    const fetchData = async () => {
+  useEffect(() => {
+    const controller = new AbortController();
+    const fetchData = async () => {
       try {
-        const params = new URLSearchParams(searchParams.toString());
-        const response = await fetch(`/api/analytics/review-interval-effectiveness?${params.toString()}`);
+        const params = new URLSearchParams(searchParams.toString());
+        const response = await fetch(
+          `/api/analytics/review-interval-effectiveness?${params.toString()}`,
+          { signal: controller.signal }
+        );
         if (!response.ok) {
           throw new Error('Failed to fetch data');
         }
         const result = await response.json();
         setData(result);
       } catch (err) {
         setError(err instanceof Error ? err.message : 'An unknown error occurred');
       }
     };
 
     fetchData();
-  }, [searchParams]);
+    return () => controller.abort();
+  }, [searchParams.toString()]);

3-12: Add explicit Chart.js types for better TS safety.

Improves editor help and guards against option typos.

-import { Scatter } from 'react-chartjs-2';
+import { Scatter } from 'react-chartjs-2';
+import type { ChartOptions, ChartData } from 'chart.js';
...
-  const chartData = {
+  const chartData: ChartData<'scatter'> = {
       datasets: [
         {
...
-  const chartOptions = {
+  const chartOptions: ChartOptions<'scatter'> = {
     responsive: true,

Also applies to: 68-105


49-51: Empty state UX.

If the API returns an empty array, show a friendly message instead of an empty canvas.

-  if (!data) {
+  if (!data) {
     return <div>Loading...</div>;
   }
+  if (data.length === 0) {
+    return <div>No review interval data for the selected filters.</div>;
+  }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6aa043c and 1a2d6fb.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • .env.example (1 hunks)
  • .gitignore (1 hunks)
  • Dockerfile (2 hunks)
  • package.json (2 hunks)
  • prisma/schema.prisma (1 hunks)
  • src/app/(dashboard)/analytics/page.tsx (1 hunks)
  • src/app/api/analytics/retention-curve/route.ts (1 hunks)
  • src/app/api/analytics/review-interval-effectiveness/route.ts (1 hunks)
  • src/app/api/analytics/session-heatmap/route.ts (1 hunks)
  • src/app/api/analytics/time-spent/route.ts (1 hunks)
  • src/app/flashcards/page.tsx (2 hunks)
  • src/app/layout.tsx (0 hunks)
  • src/components/analytics/DifficultyInsights.tsx (1 hunks)
  • src/components/analytics/Filters.tsx (1 hunks)
  • src/components/analytics/Recommendations.tsx (1 hunks)
  • src/components/analytics/RetentionCurve.tsx (1 hunks)
  • src/components/analytics/ReviewIntervalEffectiveness.tsx (1 hunks)
  • src/components/analytics/SessionHeatmap.tsx (1 hunks)
  • src/components/analytics/SubjectProgress.tsx (1 hunks)
  • src/components/analytics/TimeSpentAnalysis.tsx (1 hunks)
  • src/components/core/flash-card.tsx (1 hunks)
  • src/components/flashcards/FlashcardGenerator.tsx (2 hunks)
💤 Files with no reviewable changes (1)
  • src/app/layout.tsx
🧰 Additional context used
🧬 Code graph analysis (10)
src/components/analytics/Filters.tsx (1)
src/components/search/GlobalSearch.tsx (2)
  • SearchFilters (32-37)
  • GlobalSearch (39-308)
src/app/api/analytics/review-interval-effectiveness/route.ts (2)
src/app/api/study/record/route.ts (2)
  • POST (5-52)
  • getNextReviewInterval (55-72)
src/app/api/flashcards/[id]/route.ts (1)
  • GET (5-37)
src/app/api/analytics/retention-curve/route.ts (1)
src/app/api/study/record/route.ts (2)
  • POST (5-52)
  • getNextReviewInterval (55-72)
src/components/analytics/SessionHeatmap.tsx (1)
src/components/ui/tooltip.tsx (1)
  • Tooltip (30-30)
src/app/(dashboard)/analytics/page.tsx (1)
src/app/layout.tsx (1)
  • RootLayout (22-62)
src/app/api/analytics/session-heatmap/route.ts (2)
src/app/api/analytics/time-spent/route.ts (1)
  • GET (5-50)
src/lib/database.ts (1)
  • prisma (7-9)
src/components/analytics/ReviewIntervalEffectiveness.tsx (1)
src/app/api/study/record/route.ts (1)
  • getNextReviewInterval (55-72)
src/app/api/analytics/time-spent/route.ts (3)
src/app/api/analytics/session-heatmap/route.ts (1)
  • GET (6-47)
src/lib/database.ts (1)
  • prisma (7-9)
src/app/api/flashcards/[id]/route.ts (1)
  • GET (5-37)
src/components/core/flash-card.tsx (1)
src/components/decks/DeckCard.tsx (1)
  • DeckCard (10-51)
src/components/flashcards/FlashcardGenerator.tsx (1)
src/components/core/generate.tsx (1)
  • Generate (32-129)
🪛 dotenv-linter (3.3.0)
.env.example

[warning] 3-3: [UnorderedKey] The CLERK_SECRET_KEY key should go before the NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY key

(UnorderedKey)

🔇 Additional comments (9)
.gitignore (1)

39-41: Good call ignoring Clerk config at repo root.

This prevents accidental secret commits. Confirm CI doesn’t rely on files under /.clerk/, and ensure .env.example documents any values otherwise expected there.

src/app/flashcards/page.tsx (1)

7-7: Import looks good.

BarChart2 import aligns with the new Analytics CTA.

src/components/analytics/SubjectProgress.tsx (1)

15-23: Chart.js registration is correct

Registering scales/elements at module scope avoids repeated registration on re-renders. Good.

src/components/analytics/RetentionCurve.tsx (1)

15-23: Chart.js setup looks good

Scoped registration is clean and avoids duplicate registrations.

src/app/api/analytics/retention-curve/route.ts (1)

17-27: Positive: User scoping looks correct.

Filtering via flashcard.userId = currentUser().id prevents cross-user leakage. Good guardrail.

.env.example (1)

5-9: Good separation of client-safe vs server-only Razorpay keys.

NEXT_PUBLIC_RAZORPAY_KEY_ID is public; secret stays server-side. LGTM.

src/app/(dashboard)/analytics/page.tsx (2)

15-18: Auth gate looks good.

Redirect on missing user is correct for server components using Clerk.


21-24: Minor: pass search params to Filters (if it expects them).

If Filters uses URL state internally, ignore; otherwise pass through.

Would you like me to wire Filters to searchParams here for consistency?

src/components/analytics/ReviewIntervalEffectiveness.tsx (1)

31-37: Remove client-side interval normalization
The API endpoint at src/app/api/analytics/review-interval-effectiveness/route.ts computes each interval as

(new Date(record.createdAt).getTime() - new Date(previousRecord.createdAt).getTime())
  / (1000 * 3600 * 24)

(i.e. days), so no additional conversion is needed in the client.

@vercel
Copy link
Contributor

vercel bot commented Sep 7, 2025

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

Project Deployment Preview Comments Updated (UTC)
flash-fathom-ai Error Error Oct 19, 2025 6:22pm

💡 Enable Vercel Agent with $100 free credit for automated AI reviews

@Suraj-kumar00
Copy link
Owner

Hey @mohanish28
Please fix this PR environment is failing and also look at the coderabbit ai suggestions

@Suraj-kumar00 Suraj-kumar00 self-requested a review September 14, 2025 06:24
@Suraj-kumar00 Suraj-kumar00 added good first issue Good for newcomers gssoc2025 GSSoC 2025 Level 2 GSSoC project Level 2 labels Sep 14, 2025
@Suraj-kumar00
Copy link
Owner

Hi @mohanish28,
Is there any update on this?

@mohanish28
Copy link
Author

mohanish28 commented Sep 14, 2025

Yes, I’m working on it as you requested. It will be completed by tomorrow, as I’m currently focused on my final year project.”
@Suraj-kumar00

…fficulty and date range parameters, and added support for new indexes in the Prisma schema.
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

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

⚠️ Outside diff range comments (2)
prisma/schema.prisma (2)

125-128: Redundant index on a unique field.

@@index([razorpayOrderId]) duplicates the implicit unique index; remove it.

   @@index([userId])
-  @@index([razorpayOrderId])
   @@index([status])
   @@index([createdAt])

122-124: Set referential action for payments → users.

Define onDelete/onUpdate to avoid orphans during user deletion.

-  user                User      @relation(fields: [userId], references: [clerkUserId])
+  user                User      @relation(fields: [userId], references: [clerkUserId], onDelete: Cascade, onUpdate: Cascade)
♻️ Duplicate comments (1)
src/app/flashcards/page.tsx (1)

69-80: Fixed invalid interactive nesting; accessible labels added — nice.

The switch to Button asChild + Link resolves nested-interactive issues and adds labels for icon-only controls.

🧹 Nitpick comments (8)
src/app/flashcards/page.tsx (1)

70-79: Move aria-labels to Button (asChild) for consistency; optional prefetch tweak.

With asChild, passing aria-label on Button is the common pattern. Also consider disabling Link prefetch if these routes aren’t frequently visited.

-<Button asChild size="icon" className="bg-purple-600 hover:bg-purple-500">
-  <Link href="/generate" aria-label="Create flashcard">
+<Button asChild size="icon" className="bg-purple-600 hover:bg-purple-500" aria-label="Create flashcard">
+  <Link href="/generate" prefetch={false}>
     <Plus className="h-7 w-7" />
   </Link>
 </Button>

-<Button asChild size="icon" className="bg-blue-600 hover:bg-blue-500">
-  <Link href="/analytics" aria-label="View analytics">
+<Button asChild size="icon" className="bg-blue-600 hover:bg-blue-500" aria-label="View analytics">
+  <Link href="/analytics" prefetch={false}>
     <BarChart2 className="h-7 w-7" />
   </Link>
 </Button>
src/app/api/analytics/session-heatmap/route.ts (4)

7-10: Standardize JSON responses and disable caching on auth failures.

Use NextResponse.json and no-store to avoid accidental caching of user-specific errors.

-  if (!user) {
-    return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
-  }
+  if (!user) {
+    return NextResponse.json({ error: "Unauthorized" }, { 
+      status: 401, 
+      headers: { "Cache-Control": "no-cache, no-store, must-revalidate" } 
+    });
+  }

67-73: Micro: avoid double Date construction per row.

Small perf/readability win.

-    studySessions.forEach((session: { startTime: Date }) => {
-      const day = new Date(session.startTime).getDay();
-      const hour = new Date(session.startTime).getHours();
+    studySessions.forEach((session: { startTime: Date }) => {
+      const d = new Date(session.startTime);
+      const day = d.getDay();
+      const hour = d.getHours();
       if (heatmapData[day]) {
         heatmapData[day][hour]++;
       }
     });

75-88: Add no-store and use NextResponse.json for success and 500s.

Analytics should not be cached; also standardize JSON.

-    return new NextResponse(JSON.stringify(heatmapData), {
-      status: 200,
-      headers: { "Content-Type": "application/json" },
-    });
+    return NextResponse.json(heatmapData, {
+      status: 200,
+      headers: { "Cache-Control": "no-cache, no-store, must-revalidate" },
+    });
   } catch (error) {
     console.error("Failed to fetch session heatmap data:", error);
-    return new NextResponse(
-      JSON.stringify({ 
-        error: "Failed to fetch session heatmap data",
-        details: error instanceof Error ? error.message : "Unknown error"
-      }), 
-      { status: 500 }
-    );
+    return NextResponse.json(
+      { error: "Failed to fetch session heatmap data",
+        details: error instanceof Error ? error.message : "Unknown error" },
+      { status: 500, headers: { "Cache-Control": "no-cache, no-store, must-revalidate" } }
+    );
   }

67-73: Timezone semantics may skew heatmap hours.

getHours() uses server timezone; consider getUTCHours() or accepting a tz offset param and normalizing server-side.

src/app/api/analytics/retention-curve/route.ts (1)

126-139: Use ISO date (UTC) for stable grouping; avoid locale/timezone drift.

toLocaleDateString varies by server locale/timezone; use YYYY-MM-DD in UTC.

-    const retentionData = studyRecords.reduce((acc: { [key: string]: { correct: number; total: number } }, record: { createdAt: Date; isCorrect: boolean }) => {
-      const date = new Date(record.createdAt).toLocaleDateString();
+    const retentionData = studyRecords.reduce((acc: { [key: string]: { correct: number; total: number } }, record: { createdAt: Date; isCorrect: boolean }) => {
+      const date = new Date(record.createdAt).toISOString().slice(0, 10); // YYYY-MM-DD UTC
       if (!acc[date]) {
         acc[date] = { correct: 0, total: 0 };
       }
       acc[date].total++;
       if (record.isCorrect) {
         acc[date].correct++;
       }
       return acc;
     }, {} as { [key: string]: { correct: number; total: number } });
prisma/schema.prisma (2)

52-54: Consider per-user unique deck names.

If desired, prevent duplicate deck names for a user with a composite unique. Backfill first to avoid migration failures.

 model Deck {
   // ...
   @@index([userId])
   @@index([name])
+  @@unique([userId, name])
 }

4-4: Confirm OpenSSL binary target(s) — Dockerfile uses openssl1.1-compat

Dockerfile installs openssl1.1-compat (./Dockerfile:7,62); keep debian-openssl-1.1.x and remove debian-openssl-3.0.x from prisma/schema.prisma unless you support deployments that require OpenSSL 3.0.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1a2d6fb and 4294447.

📒 Files selected for processing (7)
  • prisma/schema.prisma (4 hunks)
  • src/app/api/analytics/retention-curve/route.ts (1 hunks)
  • src/app/api/analytics/review-interval-effectiveness/route.ts (1 hunks)
  • src/app/api/analytics/session-heatmap/route.ts (1 hunks)
  • src/app/api/analytics/time-spent/route.ts (1 hunks)
  • src/app/flashcards/page.tsx (2 hunks)
  • src/components/core/flash-card.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/api/analytics/time-spent/route.ts
  • src/components/core/flash-card.tsx
  • src/app/api/analytics/review-interval-effectiveness/route.ts
🧰 Additional context used
🧬 Code graph analysis (2)
src/app/api/analytics/session-heatmap/route.ts (2)
src/app/api/analytics/retention-curve/route.ts (1)
  • GET (19-162)
src/lib/database.ts (1)
  • prisma (7-9)
src/app/api/analytics/retention-curve/route.ts (2)
src/app/api/analytics/session-heatmap/route.ts (1)
  • GET (6-89)
src/lib/database.ts (1)
  • prisma (7-9)
🔇 Additional comments (2)
src/app/flashcards/page.tsx (1)

23-23: Resolved — /api/decks derives user from auth() and ignores client-supplied userId.
src/app/api/decks/route.ts calls const { userId } = await auth(); and a repo-wide search found no uses of searchParams.get('userId') in src/app/api; IDOR risk not present.

prisma/schema.prisma (1)

114-116: Replace plan/billingCycle Strings with enums

Replace these String fields with Plan and BillingCycle enums to enforce allowed values and simplify validation; include a migration plan and update all usages.

File: prisma/schema.prisma Lines: 114-116

  plan                String    // "basic", "pro"
  billingCycle        String    // "monthly", "yearly"
  status              PaymentStatus @default(PENDING)

Open a follow-up PR introducing Plan and BillingCycle enums and a migration plan.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (5)
prisma/schema.prisma (5)

53-53: Index on Deck.name: consider integrity/search needs.

If you want per-user unique deck names or faster case-insensitive lookups, consider either a composite unique or a functional/citext index; otherwise keep as-is.

Possible composite unique (breaking if dupes exist):

 model Deck {
   name        String
   userId      String
@@
-  @@index([name])
+  @@index([name])
+  @@unique([userId, name])
 }

87-89: Good: composite index for session listing.

[userId, startTime] aligns with common queries (user timeline). Consider adding [userId, endTime] only if you filter by end time.


101-103: Add composite “ordering” indexes for hot analytics paths.

Ordering/filters by createdAt usually scoped by sessionId or flashcardId. Composite indexes cut index scans significantly.

 model StudyRecord {
@@
   @@index([flashcardId])
   @@index([sessionId])
   @@index([createdAt])
+  @@index([sessionId, createdAt(sort: Desc)])
+  @@index([flashcardId, createdAt(sort: Desc)])
 }

106-130: Redundant index on a unique column (razorpayOrderId).

Unique already creates an index; the extra @@index duplicates it (slower migrations, no benefit).

 model Payment {
   razorpayOrderId     String    @unique
@@
-  @@index([razorpayOrderId])
   @@index([status])
   @@index([createdAt])
 }

106-130: Enum-ize plan and billingCycle (optional).

To avoid typos and ease filtering, consider enums for plan and billingCycle (you already use PaymentStatus).

+enum PaymentPlan {
+  BASIC
+  PRO
+}
+
+enum BillingCycle {
+  MONTHLY
+  YEARLY
+}
+
 model Payment {
-  plan                String    // "basic", "pro"
-  billingCycle        String    // "monthly", "yearly"
+  plan                PaymentPlan
+  billingCycle        BillingCycle
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4294447 and 8d13818.

📒 Files selected for processing (3)
  • prisma/schema.prisma (4 hunks)
  • src/app/api/analytics/retention-curve/route.ts (1 hunks)
  • src/app/api/analytics/session-heatmap/route.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/app/api/analytics/session-heatmap/route.ts
  • src/app/api/analytics/retention-curve/route.ts
🔇 Additional comments (2)
prisma/schema.prisma (2)

12-38: FKs reference clerkUserId — add migrations & backfill

prisma/schema.prisma now points relations at clerkUserId (lines 50, 70, 86, 124–125); no migration/backfill steps were found in prisma/migrations — add migrations that drop old FKs on User.id, create new FKs to User.clerkUserId, include a backfill to map existing rows to clerkUserId, and verify indexes/constraints and data integrity.


4-4: Prisma binaryTargets — looks good; confirm runtime OS/arch

Current in prisma/schema.prisma: binaryTargets = ["native", "debian-openssl-1.1.x", "debian-openssl-3.0.x"] — covers native + glibc/Debian with OpenSSL 1.1/3.0. If you deploy on Alpine (musl) add linux-musl-openssl-3.0.x (or linux-musl / linux-musl-openssl-1.1.x for older OpenSSL); for RHEL/Lambda use rhel-openssl-3.0.x. Add ARM variants (e.g., linux-arm64-openssl-3.0.x, linux-musl-arm64-openssl-3.0.x) if targeting ARM. Confirm target OS (Alpine vs glibc), OpenSSL version, and CPU arch so I can finalize the exact binaryTargets.

@mohanish28
Copy link
Author

@Suraj-kumar00 The task has been completed as per the CodeRabbit suggestion.

@Suraj-kumar00
Copy link
Owner

Hi @mohanish28,
you can see the deployment is still failing can't you do the pnpm bulid command and when you run it locally as well it should not give any errors otherwise in the deployements it will fail.

Screenshot 2025-09-19 at 11 45 46 PM

@mohanish28
Copy link
Author

i have done changes as you said can you try again?? @Suraj-kumar00

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (7)
src/app/pricing/page.tsx (2)

32-39: Build-breaker: React namespace type without import

icon: React.ReactNode can fail type-checking in TS projects where React namespace types aren’t globally available. Prefer ReactNode with a type import.

Apply:

+import type { ReactNode } from "react";
 ...
 interface PricingItem {
   plan: string;
   tagline: string;
   quota: number;
-  icon: React.ReactNode;
+  icon: ReactNode;
   popular: boolean;
   features: Feature[];

156-160: Razorpay → Result page contract mismatch

You redirect to /result?payment=success|failed, but src/app/result/page.tsx expects session_id. Users landing here see “No session ID provided.”

Either (a) change redirects to a Stripe-like flow with session_id (not applicable for Razorpay), or (b) update result/page.tsx to handle the payment query param. I’ve provided a concrete fix in that file’s comment.

Also applies to: 171-174

src/app/flashcards/[id]/page.tsx (1)

110-142: Signed‑out users get stuck on skeleton (no fetch → no setLoading(false))

When isLoaded is true and user is null, the data fetch never runs and loading stays true indefinitely.

Minimal fix: redirect or resolve loading when unauthenticated.

+  useEffect(() => {
+    if (isLoaded && !user) {
+      setLoading(false);
+      router.push("/sign-in");
+    }
+  }, [isLoaded, user, router]);

Also applies to: 156-174

src/app/result/page.tsx (1)

4-5: Handle Razorpay redirects and remove unused router

This page expects session_id (Stripe), but pricing redirects with ?payment=success|failed. Also useRouter() is declared but unused.

Apply:

-import { useRouter, useSearchParams } from 'next/navigation'
+import { useSearchParams } from 'next/navigation'
 ...
-function ResultContent() {
-    const router = useRouter()
+function ResultContent() {
     const searchParams = useSearchParams()
     const session_id = searchParams.get('session_id')
+    const payment = searchParams.get('payment') // "success" | "failed" | null
 ...
     useEffect(() => {
         const fetchCheckoutSession = async () => {
-            if (!session_id) {
-                setError('No session ID provided')
-                setLoading(false)
-                return
-            }
+            if (!session_id) {
+                // Support Razorpay flow: rely on ?payment
+                if (payment) {
+                    setSession({ payment_status: payment === 'success' ? 'paid' : 'unpaid' } as Session)
+                    setLoading(false)
+                    return
+                }
+                setError('No session ID provided')
+                setLoading(false)
+                return
+            }
 ...
-                const sessionData = await res.json()
+                const sessionData = await res.json().catch(() => ({}))
                 if (res.ok) {
                     setSession(sessionData)
                 } else {
-                    setError(sessionData.error)
+                    setError(sessionData.error || 'Failed to retrieve the session.')
                 }
 ...
-        fetchCheckoutSession()
-    }, [session_id])
+        fetchCheckoutSession()
+    }, [session_id, payment])

Also applies to: 21-28, 29-53

src/app/flashcards/page.tsx (1)

21-43: Signed‑out users get stuck on the spinner.

fetchDecks returns early when !user, so setLoading(false) never runs; the UI stays in the loading state and never shows the sign‑in prompt.

Apply:

   const fetchDecks = async () => {
-    if (!user) return;
+    if (!user) {
+      setLoading(false);
+      return;
+    }

Optional hardening: add an AbortController to cancel the fetch on unmount to avoid state updates on an unmounted component.

next.config.js (2)

34-59: Production build likely failing on SVG imports — add webpack SVGR rule.

Turbopack dev rule won’t apply during next build (webpack). Without a webpack rule, importing .svg as React components will error.

Apply this diff inside the webpack function:

   webpack: (config, { dev, isServer }) => {
     // Optimize bundle size in production
     if (!dev && !isServer) {
       config.optimization.splitChunks = {
         chunks: 'all',
         cacheGroups: {
           vendor: {
             test: /[\\/]node_modules[\\/]/,
             name: 'vendors',
             chunks: 'all',
           },
           common: {
             name: 'common',
             minChunks: 2,
             chunks: 'all',
           },
         },
       };
     }
-    
+
+    // Ensure SVGs import as React components in production (webpack build)
+    config.module = config.module || {};
+    config.module.rules = config.module.rules || [];
+    config.module.rules.push({
+      test: /\.svg$/i,
+      issuer: /\.[jt]sx?$/,
+      use: ['@svgr/webpack'],
+    });

Also ensure the dependency exists:

  • pnpm: pnpm add -D @svgr/webpack
  • npm: npm i -D @svgr/webpack

34-59: Add @svgr/webpack or remove the Turbopack SVG loader

next.config.js (turbopack.rules for '*.svg' — references '@svgr/webpack' at ~line 15) but package.json has no @svgr/webpack; add it as a devDependency (pnpm add -D @svgr/webpack) or remove/replace the loader with Next's recommended SVG handling.

🧹 Nitpick comments (18)
src/app/pricing/page.tsx (2)

18-21: Remove unused theme

const { theme } = useTheme() is never used.

-import { useTheme } from "next-themes";
 ...
-  const { theme } = useTheme();

Also applies to: 50-53


83-85: Gate verbose logs

Console logs leak payment details and add noise. Guard in dev only.

-  console.log('🚀 Creating order for:', { plan, backendPlan, billingCycle });
+  if (process.env.NODE_ENV !== 'production') {
+    console.log('🚀 Creating order for:', { plan, backendPlan, billingCycle });
+  }
 ...
-  console.log('✅ Order created:', { orderId, amount: `₹${amount/100}` });
+  if (process.env.NODE_ENV !== 'production') {
+    console.log('✅ Order created:', { orderId, amount: `₹${amount/100}` });
+  }

Also applies to: 110-112

src/app/flashcards/[id]/page.tsx (4)

49-60: Modulo by zero risk when no cards

Arrow handlers compute % flashcards.length; length 0 → NaN.

  const handleNext = useCallback(() => {
-    setCurrentIndex((prevIndex) => (prevIndex + 1) % flashcards.length);
+    if (flashcards.length === 0) return;
+    setCurrentIndex((prevIndex) => (prevIndex + 1) % flashcards.length);
     setFlip(false);
   }, [flashcards.length]);

  const handlePrevious = useCallback(() => {
-    setCurrentIndex(
-      (prevIndex) => (prevIndex - 1 + flashcards.length) % flashcards.length
-    );
+    if (flashcards.length === 0) return;
+    setCurrentIndex((prevIndex) => (prevIndex - 1 + flashcards.length) % flashcards.length);
     setFlip(false);
   }, [flashcards.length]);

47-48: Use functional state update to avoid stale flip dependency

Simpler and avoids re‑creating the callback on flip change.

-  const handleFlip = useCallback(() => setFlip(!flip), [flip]);
+  const handleFlip = useCallback(() => setFlip((v) => !v), []);

77-92: Swipe guard misfires at screen edge

if (!touchStart || !touchEnd) treats 0 as falsy; left‑edge swipes won’t register.

-  if (!touchStart || !touchEnd) return;
+  if (touchStart === null || touchEnd === null) return;

95-108: Keyboard handler should no‑op when there are no cards

Prevents NaN index when pressing arrows with empty lists.

   const handleKeyDown = (event: KeyboardEvent) => {
+    if (flashcards.length === 0) return;
     if (event.key === "ArrowRight") {
       handleNext();
 ...
-  }, [handleFlip, handleNext, handlePrevious]);
+  }, [handleFlip, handleNext, handlePrevious, flashcards.length]);
src/app/result/page.tsx (1)

4-4: Remove unused import

useRouter is no longer used after the above change.

-import { useRouter, useSearchParams } from 'next/navigation'
+import { useSearchParams } from 'next/navigation'
src/app/layout.tsx (3)

14-16: Global force‑dynamic: confirm necessity

This disables static optimization across the app, reducing cacheability. If Clerk-only pages need dynamic, consider keeping per‑page flags and dropping the global one.


33-52: Clerk key handling: simplify and avoid invalid fallback keys

Passing a fake publishableKey in dev can still trigger runtime errors. Prefer: fail fast in prod (keep the guard) and rely on env in dev by omitting the prop.

-  const clerkPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
-  if (!clerkPublishableKey) {
+  const clerkPublishableKey = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY;
+  if (!clerkPublishableKey) {
     if (process.env.NODE_ENV === 'production') {
       throw new Error(
         'NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY environment variable is required in production. ' +
         'Please set this variable in your production environment.'
       );
     } else {
       console.warn(
         '⚠️  NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY is not set. Using development fallback. ' +
         'Please set NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY in your .env.local file for proper authentication.'
       );
     }
   }
-
-  const publishableKey = clerkPublishableKey || 'pk_dev_fallback_please_set_env_var';
 ...
-        <ClerkProvider
-          publishableKey={publishableKey}
+        <ClerkProvider

Also applies to: 58-60


10-10: Dead CSS import

react-toastify CSS is unused (Toaster from shadcn is in use). Remove to shave bytes.

-import 'react-toastify/dist/ReactToastify.css'
src/app/not-found.tsx (3)

26-29: Use Next router with a safe fallback instead of raw window.history

This avoids no-op behavior when there’s no history entry and keeps navigation within the App Router.

+import { useRouter } from 'next/navigation'
 import Link from 'next/link'
 import { Button } from '@/components/ui/button'
 import { Home, ArrowLeft } from 'lucide-react'

 export default function NotFound() {
-  return (
+  const router = useRouter()
+  return (
     <div className="min-h-screen flex items-center justify-center bg-background">
@@
-          <Button variant="outline" onClick={() => window.history.back()}>
+          <Button
+            variant="outline"
+            onClick={() =>
+              (history.length > 1 ? router.back() : router.push('/'))
+            }
+          >
             <ArrowLeft className="h-4 w-4 mr-2" />
             Go Back
           </Button>

9-16: Add a main landmark and label/description hooks for better a11y

Improves screen reader navigation without changing visuals.

-    <div className="min-h-screen flex items-center justify-center bg-background">
+    <main
+      className="min-h-screen flex items-center justify-center bg-background"
+      role="main"
+      aria-labelledby="not-found-title"
+      aria-describedby="not-found-desc"
+    >
       <div className="text-center space-y-6 px-4">
@@
-          <h2 className="text-2xl font-semibold text-foreground">Page Not Found</h2>
-          <p className="text-muted-foreground max-w-md mx-auto">
+          <h2 id="not-found-title" className="text-2xl font-semibold text-foreground">Page Not Found</h2>
+          <p id="not-found-desc" className="text-muted-foreground max-w-md mx-auto">
@@
-    </div>
+    </main>

22-23: Hide decorative icons from assistive tech

Mark icons as decorative so screen readers don’t read them redundantly.

-              <Home className="h-4 w-4" />
+              <Home aria-hidden="true" className="h-4 w-4" />
@@
-            <ArrowLeft className="h-4 w-4 mr-2" />
+            <ArrowLeft aria-hidden="true" className="h-4 w-4 mr-2" />

Also applies to: 27-27

next.config.js (2)

8-10: Comment is misleading; these options don’t disable SSG. Use route-level dynamic settings instead.

trailingSlash and generateEtags don’t address Clerk+SSG issues. Prefer export const dynamic = 'force-dynamic' or export const fetchCache = 'force-no-store' on affected pages/routes.

Apply this diff to remove the no-op settings and fix the comment:

-  // Disable static generation to avoid Clerk issues during build
-  trailingSlash: false,
-  generateEtags: false,
+  // Note: Handle Clerk vs SSG at route-level (e.g., export const dynamic = 'force-dynamic')

Example for pages using Clerk:

// at top of the route/page file
export const dynamic = 'force-dynamic';
// or
export const fetchCache = 'force-no-store';

54-56: No-op alias block — remove to reduce noise.

This reassigns the same alias map without changes.

-    config.resolve.alias = {
-      ...config.resolve.alias,
-    };
src/app/api/checkout-sessions/route.ts (1)

16-30: Validate subscriptionType and base URL; avoid defaulting to monthly; fail fast on missing NEXT_PUBLIC_APP_URL.

Prevents silent mis-billing and Stripe URL errors when env is missing.

 export async function POST(req: Request) {
   const { subscriptionType } = await req.json();

-  const priceId =
-    subscriptionType === "yearly"
-      ? process.env.STRIPE_PRICE_YEARLY
-      : process.env.STRIPE_PRICE_MONTHLY;
+  if (subscriptionType !== 'yearly' && subscriptionType !== 'monthly') {
+    return NextResponse.json({ error: "Invalid subscription type" }, { status: 400 });
+  }
+  const priceId =
+    subscriptionType === "yearly"
+      ? process.env.STRIPE_PRICE_YEARLY
+      : process.env.STRIPE_PRICE_MONTHLY;
+
+  const baseUrl = process.env.NEXT_PUBLIC_APP_URL;
+  if (!baseUrl) {
+    return NextResponse.json(
+      { error: "Server misconfigured: NEXT_PUBLIC_APP_URL is missing" },
+      { status: 500 }
+    );
+  }
@@
-      success_url: `${process.env.NEXT_PUBLIC_APP_URL}/result?session_id={CHECKOUT_SESSION_ID}`,
-      cancel_url: `${process.env.NEXT_PUBLIC_APP_URL}/pricing`,
+      success_url: `${baseUrl}/result?session_id={CHECKOUT_SESSION_ID}`,
+      cancel_url: `${baseUrl}/pricing`,

Also applies to: 42-44

src/app/api/razorpay/create-order/route.ts (2)

135-135: Don’t leak config details in JSON errors.

When getRazorpayInstance() throws, the outer catch returns error.message to clients (mentions env variable names). Log details server-side; return a generic 500 message to clients.


135-135: Guard against duplicate orders (client retries/double‑clicks).

Consider deduping by checking a recent pending order for the same {userId, plan, billingCycle} before creating a new one, or enforce a uniqueness window via DB constraint/receipt strategy.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8d13818 and d5db0d7.

📒 Files selected for processing (11)
  • next.config.js (1 hunks)
  • src/app/(dashboard)/analytics/page.tsx (1 hunks)
  • src/app/(dashboard)/generate/page.tsx (1 hunks)
  • src/app/api/checkout-sessions/route.ts (3 hunks)
  • src/app/api/razorpay/create-order/route.ts (2 hunks)
  • src/app/flashcards/[id]/page.tsx (1 hunks)
  • src/app/flashcards/page.tsx (2 hunks)
  • src/app/layout.tsx (2 hunks)
  • src/app/not-found.tsx (1 hunks)
  • src/app/pricing/page.tsx (1 hunks)
  • src/app/result/page.tsx (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/app/(dashboard)/analytics/page.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
src/app/pricing/page.tsx (1)
src/app/layout.tsx (1)
  • dynamic (15-15)
src/app/(dashboard)/generate/page.tsx (6)
src/app/(dashboard)/analytics/page.tsx (1)
  • dynamic (11-11)
src/app/flashcards/[id]/page.tsx (1)
  • dynamic (28-28)
src/app/flashcards/page.tsx (1)
  • dynamic (13-13)
src/app/layout.tsx (1)
  • dynamic (15-15)
src/app/pricing/page.tsx (1)
  • dynamic (23-23)
src/app/result/page.tsx (1)
  • dynamic (13-13)
src/app/result/page.tsx (6)
src/app/(dashboard)/analytics/page.tsx (1)
  • dynamic (11-11)
src/app/(dashboard)/generate/page.tsx (1)
  • dynamic (4-4)
src/app/flashcards/[id]/page.tsx (1)
  • dynamic (28-28)
src/app/flashcards/page.tsx (1)
  • dynamic (13-13)
src/app/layout.tsx (1)
  • dynamic (15-15)
src/app/pricing/page.tsx (1)
  • dynamic (23-23)
src/app/flashcards/[id]/page.tsx (6)
src/app/(dashboard)/analytics/page.tsx (1)
  • dynamic (11-11)
src/app/(dashboard)/generate/page.tsx (1)
  • dynamic (4-4)
src/app/flashcards/page.tsx (1)
  • dynamic (13-13)
src/app/layout.tsx (1)
  • dynamic (15-15)
src/app/pricing/page.tsx (1)
  • dynamic (23-23)
src/app/result/page.tsx (1)
  • dynamic (13-13)
src/app/layout.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/flashcards/page.tsx (7)
src/app/(dashboard)/analytics/page.tsx (1)
  • dynamic (11-11)
src/app/(dashboard)/generate/page.tsx (1)
  • dynamic (4-4)
src/app/flashcards/[id]/page.tsx (1)
  • dynamic (28-28)
src/app/layout.tsx (1)
  • dynamic (15-15)
src/app/pricing/page.tsx (1)
  • dynamic (23-23)
src/app/result/page.tsx (1)
  • dynamic (13-13)
src/components/ui/button.tsx (1)
  • Button (56-56)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: GitGuardian Security Checks
🔇 Additional comments (9)
src/app/pricing/page.tsx (1)

22-24: Dynamic rendering: OK

Consistent with other pages and mitigates Clerk SSR issues. Note: since layout.tsx also sets force-dynamic globally, this is redundant; see layout comment.

src/app/(dashboard)/generate/page.tsx (1)

3-5: Dynamic rendering: OK

Matches the project-wide pattern. If layout stays force-dynamic globally, this line is redundant.

src/app/flashcards/[id]/page.tsx (1)

27-29: Dynamic rendering: OK

Consistent with Clerk usage.

src/app/result/page.tsx (1)

12-14: Dynamic rendering: OK

Aligned with other pages.

src/app/flashcards/page.tsx (3)

72-83: A11y/semantics fix confirmed — no nested interactive elements.

Using <Button asChild> with <Link> and adding aria‑labels resolves the previous issue. Looks good.


7-7: Icon imports look good.

Loader2, Plus, and BarChart2 usage is correct.


26-27: Derive userId on the server; stop passing it from the client. Server already uses await auth() to get the authenticated user (see src/app/api/decks/route.ts), so remove the client query param in src/app/flashcards/page.tsx — replace:
fetch(/api/decks?userId=${user.id})
with
fetch('/api/decks')

src/app/not-found.tsx (1)

1-34: Approve — 404 UX; imports & aliases verified

Quick verification: lucide-react ^0.428.0; next 15.4.4; tsconfig maps "@/" → ./src/; Button exists at src/components/ui/button.tsx; imports in src/app/not-found.tsx are correct. No changes required.

next.config.js (1)

11-19: Confirmed — top-level turbopack.rules is supported on Next 15.3.0+ (your project uses 15.4.4).

Next.js docs state the config moved from experimental.turbo to top-level turbopack in 15.3.0 and show the same rules example (including @svgr/webpack). (nextjs.org)

pnpm build produced no warnings for unsupported turbopack keys in your run.

Comment on lines +3 to +4
// Remove standalone output to avoid Windows symlink permission issues
// output: 'standalone',
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Don’t disable output: 'standalone' unconditionally — gate it for CI/containers.

Disabling standalone can break container/cloud deploys that expect the standalone output. Keep it off for Windows dev, but enable in CI.

Apply this diff and add the helper constant:

+const enableStandalone =
+  process.env.CI === 'true' || process.env.NEXT_STANDALONE === 'true';
 /** @type {import('next').NextConfig} */
 const nextConfig = {
-  // Remove standalone output to avoid Windows symlink permission issues
-  // output: 'standalone',
+  // Standalone for CI/containers; keep off on Windows dev
+  ...(enableStandalone && { output: 'standalone' }),

Run locally to verify deploy parity:


🏁 Script executed:

#!/bin/bash
# Show Next version and whether standalone is enabled
jq -r '.dependencies.next // .devDependencies.next' package.json
CI=true node -e "const cfg=require('./next.config.js'); console.log('output:', cfg.output||'(none)')"

Length of output: 21


Enable standalone in CI/containers (don't disable unconditionally)

Current config has no output set (CI=true test printed "output: (none)"). Gate standalone and apply:

+const enableStandalone = Boolean(process.env.CI) || process.env.NEXT_STANDALONE === 'true';
 /** @type {import('next').NextConfig} */
 const nextConfig = {
-  // Remove standalone output to avoid Windows symlink permission issues
-  // output: 'standalone',
+  // Standalone for CI/containers; keep off on Windows dev
+  ...(enableStandalone && { output: 'standalone' }),
📝 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
// Remove standalone output to avoid Windows symlink permission issues
// output: 'standalone',
const enableStandalone = Boolean(process.env.CI) || process.env.NEXT_STANDALONE === 'true';
/** @type {import('next').NextConfig} */
const nextConfig = {
// Standalone for CI/containers; keep off on Windows dev
...(enableStandalone && { output: 'standalone' }),
🤖 Prompt for AI Agents
In next.config.js around lines 3-4, the config currently comments out output:
'standalone' unconditionally; instead, enable standalone mode when running in CI
or container environments. Change the config to set output: 'standalone' when
process.env.CI === 'true' or process.env.CONTAINER === 'true' (or another CI env
flag your CI uses), and omit or leave undefined for local/dev to avoid Windows
symlink issues; implement this by computing an output value from those env vars
and including it in the exported config.

Comment on lines +4 to +14
function getStripeInstance() {
const secretKey = process.env.STRIPE_SECRET_KEY;

if (!secretKey) {
throw new Error('Stripe configuration is missing. Please set STRIPE_SECRET_KEY environment variable.');
}

return new Stripe(secretKey, {
apiVersion: "2024-06-20",
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Hardcoded Stripe apiVersion may break TypeScript build (likely deploy failure).

If the installed stripe package doesn’t include "2024-06-20" in its union, the constructor options won’t type‑check and pnpm build will fail. Quick unblocking fix: omit apiVersion and rely on account default (or gate via env).

Apply this diff:

   return new Stripe(secretKey, {
-    apiVersion: "2024-06-20",
-  });
+  });

Alternative (if you want it configurable): apiVersion: (process.env.STRIPE_API_VERSION as any) and add STRIPE_API_VERSION to .env.example.

📝 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
function getStripeInstance() {
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) {
throw new Error('Stripe configuration is missing. Please set STRIPE_SECRET_KEY environment variable.');
}
return new Stripe(secretKey, {
apiVersion: "2024-06-20",
});
}
function getStripeInstance() {
const secretKey = process.env.STRIPE_SECRET_KEY;
if (!secretKey) {
throw new Error('Stripe configuration is missing. Please set STRIPE_SECRET_KEY environment variable.');
}
return new Stripe(secretKey, {
});
}
🤖 Prompt for AI Agents
In src/app/api/checkout-sessions/route.ts around lines 4 to 14, the Stripe
client is created with a hardcoded apiVersion "2024-06-20" which can cause
TypeScript build failures if the installed stripe package types don’t include
that version; remove the apiVersion option to rely on the account default or
make it configurable by reading process.env.STRIPE_API_VERSION (cast to any for
TypeScript, e.g., apiVersion: process.env.STRIPE_API_VERSION as any) and add
STRIPE_API_VERSION to .env.example if you choose the configurable route.

⚠️ Potential issue

🧩 Analysis chain

Ensure Node.js runtime (Stripe SDK isn’t Edge-compatible).

Set export const runtime = 'nodejs' for this route (or verify no parent segment forces Edge). This avoids runtime crashes during deploy.

Add near the top of the file:

export const runtime = 'nodejs';

🏁 Script executed:

#!/bin/bash
# Verify no Edge runtime is set for this route/segment
rg -nP $'export\\s+const\\s+runtime\\s*=\\s*["\\\']edge["\\\']' src/app/api/checkout-sessions -C2 || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail
echo ">>> Searching for `export const runtime` anywhere in the repo"
rg -n --hidden --no-ignore -S "export const runtime" || true

echo
echo ">>> Searching specifically for runtime = 'edge' or 'nodejs'"
rg -n --hidden --no-ignore -S -P "export\s+const\s+runtime\s*=\s*['\"](edge|nodejs)['\"]" || true

echo
echo ">>> Showing src/app/api/checkout-sessions/route.ts (if present)"
if [ -f src/app/api/checkout-sessions/route.ts ]; then
  sed -n '1,240p' src/app/api/checkout-sessions/route.ts || true
else
  echo "src/app/api/checkout-sessions/route.ts not found"
fi

Length of output: 2200


Set Node.js runtime for this route

No runtime export found in src/app/api/checkout-sessions/route.ts — add export const runtime = 'nodejs'; at the top so the handler runs on Node (avoids Edge execution). (nextjs.org)
Stripe's official Node SDK is known to break in Edge runtimes (issues reported); force Node to prevent runtime crashes. (github.com)

🤖 Prompt for AI Agents
In src/app/api/checkout-sessions/route.ts around lines 4 to 14, there is no
runtime export so the route may run on the Edge runtime and break the Stripe
Node SDK; add an explicit runtime export at the top of the file by adding export
const runtime = 'nodejs'; (place it before any other code/exports in the file)
to force Node.js execution and prevent Stripe SDK runtime errors.

Comment on lines +65 to 67
const stripe = getStripeInstance();
const session = await stripe.checkout.sessions.retrieve(session_id);
return NextResponse.json(session);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Don’t return full Checkout Session to unauthenticated clients; sanitize response (PII risk).

checkout.sessions.retrieve can include customer details. Return only needed fields and (ideally) verify ownership via client_reference_id/metadata.

-    const session = await stripe.checkout.sessions.retrieve(session_id);
-    return NextResponse.json(session);
+    const session = await stripe.checkout.sessions.retrieve(session_id);
+    const { id, mode, status, payment_status, subscription } = session;
+    return NextResponse.json({ id, mode, status, payment_status, subscription });

Optional next step: set client_reference_id or metadata.userId in the POST create call and require it to match the requester here.

Verify if user correlation already exists:


🏁 Script executed:

#!/bin/bash
rg -nP -C2 "checkout\\.sessions\\.create\\(|client_reference_id|metadata\\s*:\\s*\\{\\s*userId" src || true

Length of output: 383


Don’t return full Checkout Session to unauthenticated clients; sanitize response and enforce ownership.

checkout.sessions.retrieve can include customer PII — return only the fields the client needs and require the requester to match client_reference_id or metadata.userId (none found in the create call).
Where: src/app/api/checkout-sessions/route.ts — retrieve at lines 65–67; create at line 33 (no client_reference_id/metadata present).

-    const session = await stripe.checkout.sessions.retrieve(session_id);
-    return NextResponse.json(session);
+    const session = await stripe.checkout.sessions.retrieve(session_id);
+    const { id, mode, status, payment_status, subscription } = session;
+    return NextResponse.json({ id, mode, status, payment_status, subscription });

Set client_reference_id or metadata.userId when creating the session and require it to match the authenticated requester on retrieval.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/api/checkout-sessions/route.ts around lines 65–67 (retrieve) and line
33 (create), you currently retrieve and return the full Stripe Checkout Session
which may expose customer PII and you also never set an owner identifier on
session creation; modify the create path to include client_reference_id or
metadata.userId tied to the authenticated user's id, and in the retrieve path
require the authenticated requester to match that client_reference_id/metadata
before returning anything; when returning the session, do not return the full
object — construct and return a sanitized object with only the fields the client
needs (e.g., id, payment_status, amount_total, currency, and any custom metadata
allowed) and omit customer, customer_details, payment_method_details and other
PII.

Comment on lines +6 to +19
// Initialize Razorpay function
function getRazorpayInstance() {
const keyId = process.env.RAZORPAY_KEY_ID;
const keySecret = process.env.RAZORPAY_KEY_SECRET;

if (!keyId || !keySecret) {
throw new Error('Razorpay configuration is missing. Please set RAZORPAY_KEY_ID and RAZORPAY_KEY_SECRET environment variables.');
}

return new Razorpay({
key_id: keyId,
key_secret: keySecret,
});
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

🧩 Analysis chain

Razorpay SDK requires Node.js runtime; set it explicitly.

Avoid Edge runtime crashes by pinning runtime.

Add near imports:

export const runtime = 'nodejs';

🏁 Script executed:

#!/bin/bash
# Check for Edge runtime on this route
rg -nP $'export\\s+const\\s+runtime\\s*=\\s*["\\\']edge["\\\']' src/app/api/razorpay/create-order -C2 || true

Length of output: 0


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "---- show file: src/app/api/razorpay/create-order/route.ts ----"
if [ -f src/app/api/razorpay/create-order/route.ts ]; then
  nl -ba src/app/api/razorpay/create-order/route.ts | sed -n '1,240p'
else
  echo "FILE_NOT_FOUND"
fi

echo "---- search for any 'export const runtime' in repo ----"
rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"][^'\"]+['\"]" --hidden -S || true

echo "---- search for 'export const runtime' in src/app/api/razorpay ----"
rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"][^'\"]+['\"]" src/app/api/razorpay -S || true

echo "---- search for Razorpay symbol usages in src ----"
rg -nP "\\bRazorpay\\b" src -S || true

echo "---- search for any runtime explicitly set to nodejs ----"
rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"]nodejs['\"]" -S || true

Length of output: 104


🏁 Script executed:

#!/bin/bash
# Retry verification without 'nl', tolerant to missing tools.
echo "---- show file: src/app/api/razorpay/create-order/route.ts ----"
if [ -f src/app/api/razorpay/create-order/route.ts ]; then
  sed -n '1,240p' src/app/api/razorpay/create-order/route.ts || true
else
  echo "FILE_NOT_FOUND"
fi

echo "---- search for 'export const runtime' anywhere ----"
if command -v rg >/dev/null 2>&1; then
  rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"][^'\"]+['\"]" --hidden -S || true
else
  grep -nR --exclude-dir=node_modules -E "export\s+const\s+runtime" . || true
fi

echo "---- search for runtime = 'edge' inside src/app/api/razorpay ----"
if command -v rg >/dev/null 2>&1; then
  rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"]edge['\"]" src/app/api/razorpay -S || true
else
  grep -nR --exclude-dir=node_modules -E "export\s+const\s+runtime\s*=\s*['\"]edge['\"]" src/app/api/razorpay || true
fi

echo "---- search for runtime = 'nodejs' ----"
if command -v rg >/dev/null 2>&1; then
  rg -nP "export\\s+const\\s+runtime\\s*=\\s*['\"]nodejs['\"]" -S || true
else
  grep -nR --exclude-dir=node_modules -E "export\s+const\s+runtime\s*=\s*['\"]nodejs['\"]" . || true
fi

echo "---- search for Razorpay usage in src ----"
if command -v rg >/dev/null 2>&1; then
  rg -nP "\\bRazorpay\\b" src -S || true
else
  grep -nR --exclude-dir=node_modules -E "\\bRazorpay\\b" src || true
fi

Length of output: 7865


Pin this route to the Node.js runtime.

This file imports the server-only Razorpay SDK and no runtime override exists; add at the top of src/app/api/razorpay/create-order/route.ts:

export const runtime = 'nodejs';

🤖 Prompt for AI Agents
In src/app/api/razorpay/create-order/route.ts around lines 6 to 19, the route
uses the server-only Razorpay SDK but lacks a runtime override; add an export to
pin this route to the Node.js runtime by exporting runtime = 'nodejs' at the top
of the file (above imports) so Next.js runs it in Node instead of the default
Edge runtime.

Comment on lines +12 to +14
// Disable static generation for this page since it uses Clerk
export const dynamic = 'force-dynamic';

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

Build break: export const dynamic is invalid in a Client Component page.

This file is a Client Component ("use client"). Next.js requires route segment config (e.g., dynamic) to live in a Server Component page/layout. Keeping it here can trigger build errors and/or be ignored, which likely explains the reported deployment/build failure.

Fix by removing the config here (you’re fetching on the client anyway), or move it to a server layout/wrapper.

Apply this minimal fix:

-// Disable static generation for this page since it uses Clerk
-export const dynamic = 'force-dynamic';

If you truly need force-dynamic for this route, move it to src/app/flashcards/layout.tsx (server) or create a small server page.tsx wrapper that renders a client FlashcardsClient component.

📝 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
// Disable static generation for this page since it uses Clerk
export const dynamic = 'force-dynamic';
🤖 Prompt for AI Agents
In src/app/flashcards/page.tsx around lines 12 to 14, the file is a Client
Component ("use client") but exports route segment config via "export const
dynamic = 'force-dynamic';" which is invalid and breaks the build; remove this
export from this client page and either (a) omit it entirely if client fetching
covers your needs, or (b) move the export to a server component such as
src/app/flashcards/layout.tsx (or create a small server page wrapper that
renders the client Flashcards component) so the route config lives in a Server
Component.

Copy link
Owner

@Suraj-kumar00 Suraj-kumar00 left a comment

Choose a reason for hiding this comment

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

Follow the coderabbit ai comment and fix those.

@Suraj-kumar00
Copy link
Owner

And the build is still failing, you can see the logs:
00:39:12.483
Creating an optimized production build ...
00:39:30.829
[webpack.cache.PackFileCacheStrategy] Serializing big strings (172kiB) impacts deserialization performance (consider using Buffer instead and decode when needed)
00:39:39.787
✓ Compiled successfully in 26.0s
00:39:39.792
Linting and checking validity of types ...
00:39:52.100
Failed to compile.
00:39:52.101
00:39:52.102
src/app/(dashboard)/analytics/page.tsx
00:39:52.102
Type error: Type '{ searchParams: { [key: string]: string | string[] | undefined; }; }' does not satisfy the constraint 'PageProps'.
00:39:52.102
Types of property 'searchParams' are incompatible.
00:39:52.102
Type '{ [key: string]: string | string[] | undefined; }' is missing the following properties from type 'Promise': then, catch, finally, [Symbol.toStringTag]
00:39:52.102
00:39:52.142
Next.js build worker exited with code: 1 and signal: null
00:39:52.184
 ELIFECYCLE  Command failed with exit code 1.
00:39:52.207
Error: Command "pnpm run build" exited with 1

Please share the screenshot of the build that should be passing on your local.

@mohanish28
Copy link
Author

image image PS C:\Users\DELL\Desktop\New folder\Flash-Fathom-AI> pnpm build

[email protected] build C:\Users\DELL\Desktop\New folder\Flash-Fathom-AI
prisma generate && next build

Prisma schema loaded from prisma\schema.prisma

✔ Generated Prisma Client (v5.22.0) to .\node_modules.pnpm@[email protected][email protected]\node_modules@prisma\client in 129ms
Start by importing your Prisma Client (See: https://pris.ly/d/importing-client)

Help us improve the Prisma ORM for everyone. Share your feedback in a short 2-min survey: https://pris.ly/orm/survey/release-5-22

▲ Next.js 15.4.4

  • Experiments (use with caution):
    · optimizePackageImports

Creating an optimized production build ...
✓ Compiled successfully in 9.0s
✓ Linting and checking validity of types
✓ Collecting page data
✓ Generating static pages (18/18)
✓ Collecting build traces
✓ Finalizing page optimization

Route (app) Size First Load JS
┌ ƒ / 4.44 kB 425 kB
├ ƒ /_not-found 113 B 421 kB
├ ƒ /about 2.17 kB 423 kB
├ ƒ /analytics 933 B 422 kB
├ ƒ /api/analytics/retention-curve 113 B 421 kB
├ ƒ /api/analytics/review-interval-effectiveness 113 B 421 kB
├ ƒ /api/analytics/session-heatmap 113 B 421 kB
├ ƒ /api/analytics/time-spent 113 B 421 kB
├ ƒ /api/checkout-sessions 113 B 421 kB
├ ƒ /api/decks 113 B 421 kB
├ ƒ /api/decks/[id] 113 B 421 kB
├ ƒ /api/decks/save 113 B 421 kB
├ ƒ /api/flashcards/[id] 113 B 421 kB
├ ƒ /api/generate 113 B 421 kB
├ ƒ /api/razorpay/create-order 113 B 421 kB
├ ƒ /api/razorpay/verify-payment 112 B 421 kB
├ ƒ /api/search 113 B 421 kB
├ ƒ /api/study/complete 112 B 421 kB
├ ƒ /api/study/record 113 B 421 kB
├ ƒ /api/study/start 113 B 421 kB
├ ƒ /api/subscribe 113 B 421 kB
├ ƒ /contact 2.58 kB 423 kB
├ ƒ /features 1.82 kB 423 kB
├ ƒ /flashcards 3.88 kB 425 kB
├ ƒ /flashcards/[id] 4.21 kB 425 kB
├ ƒ /generate 4.18 kB 425 kB
├ ƒ /generate-pro 1.08 kB 422 kB
├ ƒ /pricing 4.15 kB 425 kB
├ ƒ /result 1.57 kB 422 kB
├ ƒ /sign-in/[[...sign-in]] 113 B 421 kB
└ ƒ /sign-up/[[...sign-up]] 114 B 421 kB

  • First Load JS shared by all 421 kB
    ├ chunks/common-a623ae78fe0c9ea5.js 145 kB
    └ chunks/vendors-57333b6adb840e1e.js 274 kB
    └ other shared chunks (total) 1.96 kB

ƒ Middleware 80.5 kB

ƒ (Dynamic) server-rendered on demand

PS C:\Users\DELL\Desktop\New folder\Flash-Fathom-AI>

@mohanish28
Copy link
Author

see @Suraj-kumar00

@Suraj-kumar00
Copy link
Owner

Let' me check what causing it to fail, because in the preview environment it still fails

@mohanish28
Copy link
Author

the PR description was too long that's why it was failing can you review it once again @Suraj-kumar00

@Suraj-kumar00
Copy link
Owner

See the build error is still right there and it's not because of long description there are errors you have to fix.

Screenshot 2025-10-18 at 10 44 23 PM

@mohanish28 mohanish28 changed the title added analytics feature added analytics feature Fixes #107 Oct 18, 2025
@mohanish28
Copy link
Author

I was writting closes # but it should be Fixes # ig can you check it again it will work
pnpm build is also successful in my device :)

@Suraj-kumar00
Copy link
Owner

I was writting closes # but it should be Fixes # ig can you check it again it will work pnpm build is also successful in my device :)

But I don't see what changes you made in order to fix this?

The error still persist bro?

I share with you the logs just chatgpt it and understand what is the error and how to fix.

in this file you need to make the improvements : src/app/(dashboard)/analytics/page.tsx

@mohanish28
Copy link
Author

mohanish28 commented Oct 19, 2025

@Suraj-kumar00 as you asked i have changed the page.tsx and pushed again this time it should work
hopefully fingers cross

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d5db0d7 and 3f697a6.

📒 Files selected for processing (1)
  • src/app/(dashboard)/analytics/page.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/(dashboard)/analytics/page.tsx (5)
src/components/analytics/Filters.tsx (1)
  • Filters (8-35)
src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (10-31)
src/components/analytics/SubjectProgress.tsx (1)
  • SubjectProgress (25-47)
src/components/analytics/RetentionCurve.tsx (1)
  • RetentionCurve (25-40)
src/components/analytics/SessionHeatmap.tsx (1)
  • SessionHeatmap (23-36)
🔇 Additional comments (5)
src/app/(dashboard)/analytics/page.tsx (5)

1-9: LGTM!

Imports are appropriate for a server component analytics page with authentication and lazy-loaded chart components.


11-12: Correct use of dynamic rendering for authentication.

The force-dynamic export is appropriate for this page since currentUser() requires per-request evaluation.


14-17: LGTM!

Metadata is properly defined for SEO and browser display.


24-28: LGTM!

Authentication check is properly implemented using Clerk's currentUser() with appropriate redirect for unauthenticated users.


19-22: Based on the dependency check results, your package versions are all correctly aligned for Next.js 15.4.4 with React 19:

  • ✓ Next.js 15.4.4
  • ✓ React 19.1.1
  • ✓ TypeScript 5.9.2 (compatible with declared ^5.8.2)
  • ✓ @types/react and @types/node present

Your interface definition is correct. The Promise<...> signature for searchParams is the right pattern for Next.js 15 App Router.

The original review comment's suggestion about dependency mismatch appears unfounded—there's no version conflict here. If you're still seeing a build error about type constraints, it may be:

  1. A transient CI issue (try rerunning)
  2. A local cache issue (clear .next and reinstall)
  3. Unrelated to this file

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (3)
src/app/api/analytics/session-heatmap/route.ts (2)

7-11: Use NextResponse.json() for consistency and add cache-control headers.

Manual JSON.stringify is inconsistent with other analytics routes and the 401 response lacks cache-control headers.

Apply this diff:

   const user = await currentUser();
   if (!user) {
-    return new NextResponse(JSON.stringify({ error: "Unauthorized" }), { status: 401 });
+    return NextResponse.json({ error: "Unauthorized" }, { 
+      status: 401,
+      headers: { 
+        "Cache-Control": "no-cache, no-store, must-revalidate"
+      }
+    });
   }

89-101: Use NextResponse.json() and add cache-control to success response.

Manual JSON.stringify is inconsistent, and the success response lacks cache-control headers to prevent stale data.

Apply this diff:

-    return new NextResponse(JSON.stringify(heatmapData), {
-      status: 200,
-      headers: { "Content-Type": "application/json" },
-    });
+    return NextResponse.json(heatmapData, {
+      status: 200,
+      headers: { 
+        "Cache-Control": "no-cache, no-store, must-revalidate"
+      },
+    });
   } catch (error) {
     console.error("Failed to fetch session heatmap data:", error);
-    return new NextResponse(
-      JSON.stringify({ 
-        error: "Failed to fetch session heatmap data"
-      }), 
-      { status: 500 }
-    );
+    return NextResponse.json(
+      { error: "Failed to fetch session heatmap data" },
+      { 
+        status: 500,
+        headers: { 
+          "Cache-Control": "no-cache, no-store, must-revalidate"
+        }
+      }
+    );
   }
src/app/api/analytics/subject-progress/route.ts (1)

8-18: Extract the difficulty validation helper to a shared utility.

This is a duplicate of the same helper in retention-curve/route.ts. Extract to a shared location to follow DRY principles.

See the detailed suggestion in the review comment for src/app/api/analytics/retention-curve/route.ts at lines 8-18.

🧹 Nitpick comments (12)
src/components/analytics/SubjectProgress.tsx (2)

47-64: Type and memoize chart config to avoid re-renders and improve type safety

Use Chart.js types and useMemo so the Line component gets stable refs; also disable maintainAspectRatio for responsive containers.

+import { useMemo } from 'react';
+import type { ChartData, ChartOptions } from 'chart.js';
...
-  const chartData = {
+  const chartData = useMemo<ChartData<'line'>>(() => ({
     labels: data.labels.length > 0 ? data.labels : ['No data'],
     datasets: data.datasets.length > 0 
       ? data.datasets.map((dataset, index) => ({
         label: dataset.label,
         data: dataset.data,
         fill: false,
         borderColor: colors[index % colors.length],
         tension: 0.1,
       }))
       : [{
         label: 'No data available',
         data: [0],
         fill: false,
         borderColor: 'rgb(200, 200, 200)',
         tension: 0.1,
       }],
-  };
+  }), [data]);
 
-  const options = {
+  const options = useMemo<ChartOptions<'line'>>(() => ({
     responsive: true,
+    maintainAspectRatio: false,
     plugins: {
       legend: { position: 'top' as const },
       title: { display: true, text: 'Subject Progress (Accuracy %)' },
     },
     scales: {
       y: {
         min: 0,
         max: 100,
         ticks: {
           callback: function(value: number | string) {
             return value + '%';
           }
         }
       }
     }
-  };
+  }), []);

Also applies to: 66-88


49-56: Ensure dataset lengths align with labels (defensive normalization)

If any dataset.data length differs from labels, Chart.js will still render but tooltips/points can misalign. Consider slicing/padding to labels.length or assert equality and log a warning. I can add a small normalizer if you want.

src/components/analytics/SessionHeatmapWrapper.tsx (1)

3-9: Avoid prop-type drift by reusing the source component’s props

Import the source prop type and derive this wrapper’s props to keep them in sync.

-import { SessionHeatmap } from './SessionHeatmap';
+import { SessionHeatmap } from './SessionHeatmap';
+import type { SessionHeatmapProps } from './SessionHeatmap';
 
-interface SessionHeatmapWrapperProps {
-  data: number[][];
-}
+type SessionHeatmapWrapperProps = Pick<SessionHeatmapProps, 'data'>;
 
 export function SessionHeatmapWrapper({ data }: SessionHeatmapWrapperProps) {
   return <SessionHeatmap data={data} />;
 }
src/components/analytics/RetentionCurveWrapper.tsx (1)

3-12: Unify prop typing with RetentionCurve to prevent divergence

Derive wrapper props from RetentionCurve props.

-import { RetentionCurve } from './RetentionCurve';
+import { RetentionCurve } from './RetentionCurve';
+import type { RetentionCurveProps } from './RetentionCurve';
 
-interface RetentionCurveWrapperProps {
-  data: {
-    labels: string[];
-    retention: number[];
-  };
-}
+type RetentionCurveWrapperProps = Pick<RetentionCurveProps, 'data'>;
 
 export function RetentionCurveWrapper({ data }: RetentionCurveWrapperProps) {
   return <RetentionCurve data={data} />;
 }
src/components/analytics/SubjectProgressWrapper.tsx (1)

3-15: Reuse SubjectProgress prop typing to stay consistent

Import the underlying component’s props and derive the wrapper’s props from it.

-import { SubjectProgress } from './SubjectProgress';
+import { SubjectProgress } from './SubjectProgress';
+import type { SubjectProgressProps } from './SubjectProgress';
 
-interface SubjectProgressWrapperProps {
-  data: {
-    labels: string[];
-    datasets: Array<{
-      label: string;
-      data: number[];
-    }>;
-  };
-}
+type SubjectProgressWrapperProps = Pick<SubjectProgressProps, 'data'>;
 
 export function SubjectProgressWrapper({ data }: SubjectProgressWrapperProps) {
   return <SubjectProgress data={data} />;
 }
src/components/analytics/Filters.tsx (3)

27-31: Debounce and prefer replace for query changes to reduce route churn

On each keystroke you push a new history entry and trigger rerenders. Use replace; optionally debounce for text inputs.

-  const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
+  const handleFilterChange = (event: React.ChangeEvent<HTMLInputElement>) => {
     const params = new URLSearchParams(searchParams.toString());
     params.set(event.target.name, event.target.value);
-    router.push(`?${params.toString()}`);
+    router.replace(`?${params.toString()}`);
   };

If you want, I can add a tiny 300ms debounce utility for subject/difficulty.


15-23: Validate timezone before setting; fall back if invalid

Use your timezone util to guard against malformed values in the URL.

+import { isValidTimezone } from '@/lib/utils/timezone';
...
   useEffect(() => {
     const currentTimezone = searchParams.get('timezone');
     const userTimezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
     
-    // Only set timezone if it's absent, preserving user selections on remounts
-    if (!currentTimezone) {
+    // Set/repair timezone only when absent or invalid
+    if (!currentTimezone || !isValidTimezone(currentTimezone)) {
       const params = new URLSearchParams(searchParams.toString());
       params.set('timezone', userTimezone);
       router.replace(`?${params.toString()}`);
     }

40-42: “Date Range” UI is a single date input

If backend expects a range, split into startDate/endDate or switch to a range picker. Otherwise, rename the label to “Date”.

Would you confirm what the API expects for date filtering?

src/components/analytics/SessionHeatmap.tsx (1)

84-86: Guard against empty dayData array in Math.max.

If dayData is an empty array, Math.max(...dayData) returns -Infinity, and indexOf will return -1, resulting in peakHour being 'N/A'. While the fallback works, the check at line 77 should catch this earlier.

Consider tightening the defensive check:

             // Defensive checks for dayData
-            if (!dayData || !Array.isArray(dayData) || dayData.length === 0) {
+            if (!dayData || !Array.isArray(dayData) || dayData.length !== 24) {
               return [
                 `Sessions: ${context.parsed?.y ?? 0}`,
                 `Peak hour: N/A`,
               ];
             }
src/components/analytics/RetentionCurve.tsx (2)

32-46: Consider displaying a message when data is empty instead of rendering a placeholder chart.

The fallback ['No data'] and [0] renders a chart with a single point, which may confuse users. A clear "No data available" message would be more user-friendly.

Apply this diff to show a message when data is empty:

 export function RetentionCurve({ data }: RetentionCurveProps) {
+  // Show message when no data
+  if (data.labels.length === 0 || data.retention.length === 0) {
+    return (
+      <div className="flex items-center justify-center p-8 text-muted-foreground">
+        <p>No retention data available</p>
+      </div>
+    );
+  }
+
   // Transform data for chart
   const chartData = {
-    labels: data.labels.length > 0 ? data.labels : ['No data'],
+    labels: data.labels,
     datasets: [
       {
         label: 'Retention Rate',
-        data: data.retention.length > 0 ? data.retention : [0],
+        data: data.retention,
         fill: false,
-        borderColor: data.retention.length > 0 ? 'rgb(75, 192, 192)' : 'rgb(200, 200, 200)',
+        borderColor: 'rgb(75, 192, 192)',
         backgroundColor: 'rgba(75, 192, 192, 0.5)',
         tension: 0.1,
       },

64-66: Ensure type safety in the tick callback.

The value parameter is typed as number | string, but concatenating with '%' assumes it's always safe to stringify. Add explicit handling for string values.

Apply this diff to handle both types explicitly:

           callback: function(value: number | string) {
-            return value + '%';
+            return typeof value === 'number' ? `${value}%` : `${value}%`;
           }
src/app/api/analytics/session-heatmap/route.ts (1)

18-31: Standardize header casing for consistency.

Line 27 uses lowercase 'cache-control' while other routes in the PR use 'Cache-Control'. HTTP headers are case-insensitive, but consistency improves maintainability.

Apply this diff:

       status: 400,
       headers: { 
-        "cache-control": "no-store"
+        "Cache-Control": "no-cache, no-store, must-revalidate"
       }

Also apply the same pattern to lines 41 and 48.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3f697a6 and 66b80e8.

📒 Files selected for processing (12)
  • src/app/(dashboard)/analytics/page.tsx (1 hunks)
  • src/app/api/analytics/retention-curve/route.ts (1 hunks)
  • src/app/api/analytics/session-heatmap/route.ts (1 hunks)
  • src/app/api/analytics/subject-progress/route.ts (1 hunks)
  • src/components/analytics/Filters.tsx (1 hunks)
  • src/components/analytics/RetentionCurve.tsx (1 hunks)
  • src/components/analytics/RetentionCurveWrapper.tsx (1 hunks)
  • src/components/analytics/SessionHeatmap.tsx (1 hunks)
  • src/components/analytics/SessionHeatmapWrapper.tsx (1 hunks)
  • src/components/analytics/SubjectProgress.tsx (1 hunks)
  • src/components/analytics/SubjectProgressWrapper.tsx (1 hunks)
  • src/lib/utils/timezone.ts (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
src/components/analytics/SessionHeatmapWrapper.tsx (1)
src/components/analytics/SessionHeatmap.tsx (1)
  • SessionHeatmap (30-108)
src/app/(dashboard)/analytics/page.tsx (5)
src/components/analytics/Filters.tsx (1)
  • Filters (9-50)
src/components/common/LoadingSpinner.tsx (1)
  • LoadingSpinner (10-31)
src/components/analytics/SubjectProgressWrapper.tsx (1)
  • SubjectProgressWrapper (13-15)
src/components/analytics/RetentionCurveWrapper.tsx (1)
  • RetentionCurveWrapper (10-12)
src/components/analytics/SessionHeatmapWrapper.tsx (1)
  • SessionHeatmapWrapper (7-9)
src/components/analytics/SubjectProgressWrapper.tsx (1)
src/components/analytics/SubjectProgress.tsx (1)
  • SubjectProgress (45-91)
src/app/api/analytics/retention-curve/route.ts (3)
src/app/api/analytics/subject-progress/route.ts (1)
  • GET (19-231)
src/lib/utils/timezone.ts (2)
  • isValidTimezone (41-48)
  • formatDateInTimezone (9-12)
src/lib/database.ts (1)
  • prisma (7-9)
src/app/api/analytics/session-heatmap/route.ts (2)
src/lib/utils/timezone.ts (3)
  • isValidTimezone (41-48)
  • getDayInTimezone (20-23)
  • getHourInTimezone (31-34)
src/lib/database.ts (1)
  • prisma (7-9)
src/components/analytics/RetentionCurveWrapper.tsx (1)
src/components/analytics/RetentionCurve.tsx (1)
  • RetentionCurve (32-73)
src/app/api/analytics/subject-progress/route.ts (3)
src/app/api/analytics/retention-curve/route.ts (1)
  • GET (20-183)
src/lib/utils/timezone.ts (2)
  • isValidTimezone (41-48)
  • formatDateInTimezone (9-12)
src/lib/database.ts (1)
  • prisma (7-9)
🔇 Additional comments (6)
src/components/analytics/Filters.tsx (1)

1-12: No action needed. Analytics page already uses correct Next 15 Promise typing.

The src/app/(dashboard)/analytics/page.tsx already correctly implements the Next 15 App Router pattern with searchParams: Promise<...> (line 90) and properly awaits it (line 97). No changes required—the codebase is already compliant.

Likely an incorrect or invalid review comment.

src/app/api/analytics/retention-curve/route.ts (1)

20-183: LGTM! Robust input validation and error handling.

The route implementation is solid:

  • Comprehensive input validation (timezone, difficulty, dateRange) with descriptive errors
  • Proper authentication and authorization
  • Efficient Prisma query with select to fetch only needed columns
  • Timezone-aware date formatting using the utility functions
  • Consistent error responses with no-cache headers
src/lib/utils/timezone.ts (2)

1-48: LGTM! Clean timezone utility implementation.

The utilities are well-documented and provide consistent timezone handling across the analytics routes. The use of isValidTimezone by calling routes ensures invalid timezones are caught early.


9-12: Code is correct—timeZone option is necessary, not redundant.

The timeZone option must be passed to format() after calling toZonedTime because toZonedTime returns a Date whose clock fields reflect the target zone but a JS Date does not carry zone metadata, so format cannot know which IANA zone to use for tokens like z/zzz/zzzz. The current implementation is correct. Alternatively, formatInTimeZone combines both operations if you prefer a single function call.

Likely an incorrect or invalid review comment.

src/app/(dashboard)/analytics/page.tsx (1)

105-130: LGTM! Clean layout with proper Suspense boundaries.

The responsive grid layout and Suspense wrappers provide a good user experience with progressive loading. The component structure is clean and maintainable.

src/app/api/analytics/subject-progress/route.ts (1)

120-231: LGTM! Well-structured subject progress endpoint.

The implementation is robust:

  • Properly fetches deck relation data (needed for grouping by subject)
  • Timezone-aware date formatting for accurate aggregation
  • Chart-friendly output format with aligned datasets
  • Comprehensive validation and error handling
  • Consistent with other analytics routes

@Suraj-kumar00
Copy link
Owner

@Suraj-kumar00 as you asked i have changed the page.tsx and pushed again this time it should work hopefully fingers cross

I don't believe in fingers crossed bro it's wrong thing to do.

BTW, the build is still failing I think you're not fixing it or not able to do if you need any help bro let me know...

@mohanish28 mohanish28 closed this Oct 20, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

good first issue Good for newcomers gssoc2025 GSSoC 2025 Level 2 GSSoC project Level 2

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Enhanced Study Analytics

2 participants