-
Notifications
You must be signed in to change notification settings - Fork 26
added analytics feature Fixes #107 #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
|
@mohanish28 is attempting to deploy a commit to the devsuraj Team on Vercel. A member of the Team first needs to authorize it. |
WalkthroughAdds 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 Changes
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)
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")
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Poem
Pre-merge checks and finishing touches❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
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: SetNullwithuserId String?.
31-38: Add mandatory backfill migration forclerkUserId
Create a one-time migration (e.g. in prisma/migrations/) that populates theuserIdcolumn on Deck, Flashcard, StudySession and Payment tables from the existingUser.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-compatAdditionally, 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 keystrokeDebounce 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 valuesInitialize 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 valuesUse 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 dateName 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 filtersReplace 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 unitsIf 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 dateRangenew 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 deriveretentionfrom a sortedlabelsarray 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 withNEXT_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 dropanyand avoid implicitanybugs.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.tsto 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/dynamicwithloading.-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
tzquery 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.
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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 correctRegistering scales/elements at module scope avoids repeated registration on re-renders. Good.
src/components/analytics/RetentionCurve.tsx (1)
15-23: Chart.js setup looks goodScoped 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().idprevents cross-user leakage. Good guardrail..env.example (1)
5-9: Good separation of client-safe vs server-only Razorpay keys.
NEXT_PUBLIC_RAZORPAY_KEY_IDis 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
FilterstosearchParamshere for consistency?src/components/analytics/ReviewIntervalEffectiveness.tsx (1)
31-37: Remove client-side interval normalization
The API endpoint atsrc/app/api/analytics/review-interval-effectiveness/route.tscomputes eachintervalas(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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
💡 Enable Vercel Agent with $100 free credit for automated AI reviews |
|
Hey @mohanish28 |
|
Hi @mohanish28, |
|
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.” |
…fficulty and date range parameters, and added support for new indexes in the Prisma schema.
There was a problem hiding this 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-compatDockerfile 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
📒 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 callsconst { userId } = await auth();and a repo-wide search found no uses ofsearchParams.get('userId')in src/app/api; IDOR risk not present.prisma/schema.prisma (1)
114-116: Replace plan/billingCycle Strings with enumsReplace 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.
…n session heatmap route
There was a problem hiding this 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
📒 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 & backfillprisma/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/archCurrent 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) addlinux-musl-openssl-3.0.x(orlinux-musl/linux-musl-openssl-1.1.xfor older OpenSSL); for RHEL/Lambda userhel-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.
|
@Suraj-kumar00 The task has been completed as per the CodeRabbit suggestion. |
|
Hi @mohanish28,
|
|
i have done changes as you said can you try again?? @Suraj-kumar00 |
There was a problem hiding this 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.ReactNodecan fail type-checking in TS projects whereReactnamespace types aren’t globally available. PreferReactNodewith 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 mismatchYou redirect to
/result?payment=success|failed, butsrc/app/result/page.tsxexpectssession_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) updateresult/page.tsxto handle thepaymentquery 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 → nosetLoading(false))When
isLoadedis true anduseris null, the data fetch never runs andloadingstays 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 routerThis page expects
session_id(Stripe), but pricing redirects with?payment=success|failed. AlsouseRouter()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.
fetchDecksreturns early when!user, sosetLoading(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
AbortControllerto 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.svgas React components will error.Apply this diff inside the
webpackfunction: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 loadernext.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 unusedtheme
const { theme } = useTheme()is never used.-import { useTheme } from "next-themes"; ... - const { theme } = useTheme();Also applies to: 50-53
83-85: Gate verbose logsConsole 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 cardsArrow 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 staleflipdependencySimpler and avoids re‑creating the callback on
flipchange.- 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 cardsPrevents 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
useRouteris 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 necessityThis 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 keysPassing a fake
publishableKeyin 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} + <ClerkProviderAlso applies to: 58-60
10-10: Dead CSS import
react-toastifyCSS 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.historyThis 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 a11yImproves 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 techMark 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.
trailingSlashandgenerateEtagsdon’t address Clerk+SSG issues. Preferexport const dynamic = 'force-dynamic'orexport 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 returnserror.messageto 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
📒 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: OKConsistent 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: OKMatches 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: OKConsistent with Clerk usage.
src/app/result/page.tsx (1)
12-14: Dynamic rendering: OKAligned 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, andBarChart2usage 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 verifiedQuick 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-levelturbopack.rulesis supported on Next 15.3.0+ (your project uses 15.4.4).Next.js docs state the config moved from
experimental.turboto top-levelturbopackin 15.3.0 and show the samerulesexample (including@svgr/webpack). (nextjs.org)pnpm build produced no warnings for unsupported turbopack keys in your run.
| // Remove standalone output to avoid Windows symlink permission issues | ||
| // output: 'standalone', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
| // 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.
| 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", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
🧩 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 || trueLength 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"
fiLength 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.
| const stripe = getStripeInstance(); | ||
| const session = await stripe.checkout.sessions.retrieve(session_id); | ||
| return NextResponse.json(session); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 || trueLength 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.
| // 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, | ||
| }); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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 || trueLength 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 || trueLength 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
fiLength 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.
| // Disable static generation for this page since it uses Clerk | ||
| export const dynamic = 'force-dynamic'; | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| // 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.
Suraj-kumar00
left a comment
There was a problem hiding this 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.
|
And the build is still failing, you can see the logs: Please share the screenshot of the build that should be passing on your local. |
PS C:\Users\DELL\Desktop\New folder\Flash-Fathom-AI> pnpm 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 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
Creating an optimized production build ... Route (app) Size First Load JS
ƒ Middleware 80.5 kB ƒ (Dynamic) server-rendered on demand PS C:\Users\DELL\Desktop\New folder\Flash-Fathom-AI> |
|
see @Suraj-kumar00 |
|
Let' me check what causing it to fail, because in the preview environment it still fails |
|
the PR description was too long that's why it was failing can you review it once again @Suraj-kumar00 |
|
I was writting closes # but it should be Fixes # ig can you check it again it will work |
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 : |
|
@Suraj-kumar00 as you asked i have changed the page.tsx and pushed again this time it should work |
There was a problem hiding this 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
📒 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-dynamicexport is appropriate for this page sincecurrentUser()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 forsearchParamsis 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:
- A transient CI issue (try rerunning)
- A local cache issue (clear
.nextand reinstall)- Unrelated to this file
There was a problem hiding this 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.stringifyis 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.stringifyis 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.tsat 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 safetyUse 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 propsImport 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 divergenceDerive 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 consistentImport 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 preferreplacefor query changes to reduce route churnOn 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 invalidUse 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 inputIf 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
dayDatais an empty array,Math.max(...dayData)returns-Infinity, andindexOfwill return -1, resulting inpeakHourbeing '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
valueparameter is typed asnumber | 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
📒 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.tsxalready correctly implements the Next 15 App Router pattern withsearchParams: 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
selectto 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
isValidTimezoneby calling routes ensures invalid timezones are caught early.
9-12: Code is correct—timeZone option is necessary, not redundant.The
timeZoneoption must be passed toformat()after callingtoZonedTimebecausetoZonedTimereturns a Date whose clock fields reflect the target zone but a JS Date does not carry zone metadata, soformatcannot know which IANA zone to use for tokens like z/zzz/zzzz. The current implementation is correct. Alternatively,formatInTimeZonecombines 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
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... |




added analytics feature
Fixes #107
Summary by CodeRabbit
New Features
Bug Fixes
Chores