Full review#2
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 12
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
🟡 Minor comments (9)
components/LoadingOverlay.tsx-8-15 (1)
8-15:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winExpose the loading state to assistive technologies.
The overlay is visual-only right now; add status semantics so screen readers announce progress.
Suggested patch
- <div className="loading-wrapper"> + <div className="loading-wrapper" role="status" aria-live="polite" aria-busy="true"> <div className="loading-shadow-wrapper bg-white shadow-soft-lg"> <div className="loading-shadow"> - <Loader2 className="loading-animation w-12 h-12 text-[`#663820`]" /> + <Loader2 className="loading-animation w-12 h-12 text-[`#663820`]" aria-hidden="true" /> <h2 className="loading-title">Synthesizing Your Book</h2> <p className="text-[`#777`] text-center max-w-xs"> Please wait while we process your PDF and prepare your interactive literary experience. </p> </div> </div> </div>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/LoadingOverlay.tsx` around lines 8 - 15, The overlay is currently visual-only; update LoadingOverlay (around the loading-wrapper / loading-shadow / loading-title elements and the Loader2 usage) to expose status semantics by adding role="status" and aria-live="polite" (and aria-busy="true" while processing) to the container, and include an offscreen text node (e.g., visually-hidden / sr-only text) that echoes the loading message ("Synthesizing Your Book…") so screen readers announce progress; ensure to remove or set aria-busy="false" when loading completes.components/ui/sonner.tsx-13-40 (1)
13-40:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse the custom
Toasterwrapper fromcomponents/ui/sonnerinstead of importing directly from"sonner".
app/layout.tsx(line 15) importsToasterfrom"sonner"directly, bypassing the custom wrapper and losing its theme integration, custom icons, and styling customizations.Change the import to:
import { Toaster } from "@/components/ui/sonner";🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ui/sonner.tsx` around lines 13 - 40, app/layout.tsx currently imports Toaster directly from the third-party "sonner" package, bypassing the custom Toaster wrapper (the Toaster component defined in components/ui/sonner) and losing theme, icons, and style overrides; update the import in app/layout.tsx to import the Toaster exported by the custom wrapper module (the Toaster component shown in the diff) instead of from "sonner" so the app uses the wrapped Toaster with theme integration and custom icons/styles.components/ui/form.tsx-28-30 (1)
28-30:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winContext guard is currently ineffective.
Because both contexts default to
{}casts, Line 52'sif (!fieldContext)never triggers. Make contexts nullable and guard against missing providers explicitly.Suggested fix
-const FormFieldContext = React.createContext<FormFieldContextValue>( - {} as FormFieldContextValue -) +const FormFieldContext = React.createContext<FormFieldContextValue | null>(null) -const FormItemContext = React.createContext<FormItemContextValue>( - {} as FormItemContextValue -) +const FormItemContext = React.createContext<FormItemContextValue | null>(null) const useFormField = () => { const fieldContext = React.useContext(FormFieldContext) const itemContext = React.useContext(FormItemContext) + if (!fieldContext?.name) { + throw new Error("useFormField should be used within <FormField>") + } + if (!itemContext?.id) { + throw new Error("useFormField should be used within <FormItem>") + } const { getFieldState } = useFormContext() const formState = useFormState({ name: fieldContext.name }) const fieldState = getFieldState(fieldContext.name, formState) - - if (!fieldContext) { - throw new Error("useFormField should be used within <FormField>") - } - const { id } = itemContextAlso applies to: 52-57, 72-74
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/ui/form.tsx` around lines 28 - 30, Change the contexts to be nullable and update the runtime guards: replace the current createContext casts (e.g., FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue)) with nullable defaults such as React.createContext<FormFieldContextValue | null>(null) for both FormFieldContext and the sibling context, then update all consumers that call useContext (the places with the current if (!fieldContext) check) to accept possibly null types and keep the explicit guard (if (!fieldContext) throw new Error(...) or return) so the guard actually triggers; ensure function/component signatures (types) that destructure context values are adjusted to handle the nullable return from useContext until the guard validates non-null.types.d.ts-5-5 (1)
5-5:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUse named Zod import for consistency with the rest of the codebase.
The codebase uses
import { z } from 'zod'elsewhere (e.g.,lib/zod.ts). Update line 5 to match this pattern for consistency.Suggested fix
-import z from 'zod'; +import { z } from 'zod';🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@types.d.ts` at line 5, The current default import "import z from 'zod'" in types.d.ts should be changed to the named import used elsewhere: replace it with "import { z } from 'zod'" so that the file uses the same Zod symbol as the rest of the codebase (see usages of z and the pattern in lib/zod.ts); update the import statement only — no other code changes required.lib/actions/session.actions.ts-6-6 (1)
6-6:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winDuplicate import of
getCurrentBillingPeriodStart.
getCurrentBillingPeriodStartis imported at the top level (line 6) and again dynamically (line 14). The top-level import is unused since the dynamic import shadows it within the function scope. Remove the top-level import to avoid confusion.🔧 Fix duplicate import
import {EndSessionResult, StartSessionResult} from "@/types"; import {connectToDatabase} from "@/database/mongoose"; import VoiceSession from "@/database/models/voice-session.model"; -import {getCurrentBillingPeriodStart} from "@/lib/subscription-constants";Also applies to: 14-14
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/actions/session.actions.ts` at line 6, Remove the unused top-level import of getCurrentBillingPeriodStart from the module so it’s only imported dynamically inside the function where it’s actually used; locate the top-level import statement for getCurrentBillingPeriodStart in session.actions.ts and delete it, leaving the dynamic import (the one at line 14) as the sole import to avoid shadowing and confusion.lib/utils.ts-87-91 (1)
87-91:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
formatDurationmay produce unexpected output with fractional seconds.If
secondscontains a decimal (e.g.,91.5), the modulo operation will preserve the fraction (31.5→ displays as1:31.5). Consider flooring both values for consistent MM:SS display.🔧 Handle fractional seconds
export const formatDuration = (seconds: number): string => { - const mins = Math.floor(seconds / 60); - const secs = seconds % 60; + const totalSecs = Math.floor(seconds); + const mins = Math.floor(totalSecs / 60); + const secs = totalSecs % 60; return `${mins}:${secs.toString().padStart(2, '0')}`; };🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/utils.ts` around lines 87 - 91, formatDuration currently uses the raw seconds value which allows fractional seconds to leak into the seconds portion (e.g., 91.5 -> "1:31.5"); change the implementation in formatDuration to first normalize to an integer (e.g., const total = Math.floor(seconds)) and then compute mins = Math.floor(total / 60) and secs = total % 60 so the output is always MM:SS with seconds zero-padded via padStart on secs.toString().lib/actions/session.actions.ts-25-28 (1)
25-28:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winUnnecessary
revalidatePathon billing limit check.Calling
revalidatePath("/")when the user hits their session limit doesn't make sense—no data was mutated. This adds unnecessary overhead and could cause confusing UI refreshes. Revalidation should only occur after successful data changes.🔧 Remove unnecessary revalidation
if (sessionCount >= limits.maxSessionsPerMonth) { - const { revalidatePath } = await import("next/cache"); - revalidatePath("/"); - return { success: false,🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/actions/session.actions.ts` around lines 25 - 28, The if-block that checks sessionCount >= limits.maxSessionsPerMonth should not import or call revalidatePath — remove the dynamic import of "next/cache" and the revalidatePath("/") call inside that branch (located around the check using sessionCount and limits.maxSessionsPerMonth) so no unnecessary revalidation occurs when only a billing limit is reached; ensure you only keep the limit check and any user-facing handling (e.g., throwing or returning the error) without triggering cache revalidation.components/Navbar.tsx-23-24 (1)
23-24:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winFix the logo alt text typo.
Line 23 uses
"Bookfied", so screen readers will announce the brand name incorrectly.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/Navbar.tsx` around lines 23 - 24, The alt text on the logo Image is misspelled ("Bookfied") causing incorrect screen-reader output; update the Image component's alt prop in Navbar.tsx (the <Image ... alt="Bookfied" ... /> instance) to the correct brand string "Bookified" so it matches the visible .logo-text and improves accessibility.app/globals.css-974-993 (1)
974-993:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRemove or normalize the commented-out Clerk block.
Lines 974-993 already trigger stylelint
comment-whitespace-inside, so this file will keep failing lint until the block is deleted or rewritten as valid comments.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/globals.css` around lines 974 - 993, Remove or normalize the commented-out Clerk CSS block so stylelint no longer flags comment-whitespace-inside: either delete the entire block shown (the .clerk-pricing-container :deep(.cl-pricing-table), :deep(.cl-pricing-table-header), and :deep(.cl-pricing-plan-card) rules and their hover rule) or rewrite it as valid CSS comments using standard /* ... */ syntax with proper spacing (e.g., start with "/* " and end with " */") and no nested comment markers like "/*!*" or "*!*/", ensuring selectors like .clerk-pricing-container :deep(.cl-pricing-table) retain correct formatting if kept.
🧹 Nitpick comments (7)
components/Transcript.tsx (1)
44-45: ⚡ Quick winMark the transcript stream as a live log region.
This improves screen-reader behavior when new messages are appended.
Suggested patch
- <div ref={scrollRef} className="transcript-messages overflow-y-auto pr-2 flex-1"> + <div + ref={scrollRef} + className="transcript-messages overflow-y-auto pr-2 flex-1" + role="log" + aria-live="polite" + aria-relevant="additions text" + >🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/Transcript.tsx` around lines 44 - 45, The transcript container div currently referenced by scrollRef and rendering messages should be marked as a live log region for screen readers; update the div (the one with ref={scrollRef} and className="transcript-messages ...") to include accessibility attributes such as role="log", aria-live="polite" (or "assertive" if immediate interruption is desired), and aria-atomic="false" so assistive tech announces new appended messages appropriately.components/HeroSection.tsx (1)
47-69: ⚡ Quick winUse ordered-list semantics for the numbered steps.
This is a step-by-step sequence, so
<ol>is a better semantic fit than<ul>.Suggested patch
- <ul className="space-y-6"> + <ol className="space-y-6"> <li className="library-step-item"> ... </li> <li className="library-step-item"> ... </li> <li className="library-step-item"> ... </li> - </ul> + </ol>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/HeroSection.tsx` around lines 47 - 69, Replace the non-semantic unordered list with an ordered list for the step sequence: change the <ul className="space-y-6"> wrapper to an <ol> and keep the existing <li className="library-step-item"> children and their inner elements (the numbered div, h3 with class library-step-title, and p with class library-step-description); also ensure any CSS that targets ul (e.g., spacing or list-style) is adjusted to support <ol> (add list-style or list-decimal as needed) so visual numbering and spacing remain correct in the HeroSection component.database/models/voice-session.model.ts (1)
5-5: 💤 Low valueRedundant individual index on
clerkId.The compound index
{ clerkId: 1, billingPeriodStart: 1 }on line 13 already covers queries that filter only byclerkId. The individual index onclerkId(line 5) is therefore redundant and adds storage/write overhead.♻️ Remove redundant index
- clerkId: { type: String, required: true, index: true }, + clerkId: { type: String, required: true },Also applies to: 10-10, 13-13
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/models/voice-session.model.ts` at line 5, Remove the redundant single-field index on clerkId: the field definition clerkId: { type: String, required: true, index: true } creates an individual index that is already covered by the compound index { clerkId: 1, billingPeriodStart: 1 } defined elsewhere in the model; update the clerkId field to remove index: true so only the compound index (the one that includes clerkId and billingPeriodStart) remains to avoid duplicate index overhead.lib/actions/session.actions.ts (1)
55-62: ⚡ Quick winMissing validation for
durationSecondsparameter.
durationSecondsis accepted directly from the caller without validation. Negative values or extremely large numbers could be stored. Consider adding bounds checking.♻️ Add validation
export const endVoiceSession = async (sessionId: string, durationSeconds: number): Promise<EndSessionResult> => { try { + if (durationSeconds < 0 || !Number.isFinite(durationSeconds)) { + return { success: false, error: 'Invalid duration value.' }; + } + await connectToDatabase();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@lib/actions/session.actions.ts` around lines 55 - 62, The endVoiceSession function accepts durationSeconds without validation; add input validation before calling VoiceSession.findByIdAndUpdate to reject or clamp invalid values (e.g., ensure durationSeconds is a finite number, >= 0 and <= a sensible max like several hours or a configured MAX_SESSION_DURATION_SECONDS), and throw or return a clear error (or normalize the value) if it fails validation; update endVoiceSession to validate durationSeconds and only proceed with the DB update when the value passes checks.app/(root)/page.tsx (1)
1-1: 💤 Low valueUnnecessary React import with React 19 JSX transform.
With React 19 and the new JSX transform enabled (standard in Next.js 15+), explicit
import React from "react"is not required for JSX usage.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@app/`(root)/page.tsx at line 1, The file contains an unnecessary top-level import "import React from \"react\"" which is not required with the React 19/new JSX transform; remove that import line and ensure no other references to the React symbol remain in the module (e.g., check for direct uses of React.*); if any runtime types are needed, import specific symbols instead of the default React import.database/models/book.model.ts (1)
4-16: 💤 Low valueConsider adding an index on
clerkId.If queries frequently filter books by user (e.g., "get all books for a user"), an index on
clerkIdwould improve performance. Theslugfield already has implicit indexing viaunique: true.♻️ Suggested index addition
const BookSchema = new Schema<IBook>({ - clerkId: { type: String, required: true }, + clerkId: { type: String, required: true, index: true }, title: { type: String, required: true },🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@database/models/book.model.ts` around lines 4 - 16, Add a single-field index on clerkId to the BookSchema to speed up queries that filter by user; update the schema definition (BookSchema) by adding an index for the clerkId field (e.g., using schema.index({ clerkId: 1 }) or the equivalent within the Schema options) so queries against IBook documents by clerkId are covered efficiently while keeping the existing unique index on slug intact.components/FileUploader.tsx (1)
47-47: 💤 Low valueType safety consideration for
valueassertion.The
value as Filecast on line 71 assumesvalueis always aFilewhenisUploadedis true. If the form state could ever hold a non-File truthy value, this would cause a runtime error.Consider adding a type guard if the field could receive values from other sources (e.g., form reset, default values).
Also applies to: 71-71
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@components/FileUploader.tsx` at line 47, The current check uses const isUploaded = !!value and later casts value with (value as File), which can crash if value is a truthy non-File; add a proper type guard before treating it as a File (e.g., implement an isFile(value): value is File that checks value instanceof File and use it wherever you currently use isUploaded or (value as File)), update the code paths that rely on (value as File) to first call isFile(value) and handle non-File truthy values with a safe fallback (e.g., treat as not uploaded or skip file-specific logic) so all uses of value are type-safe.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@app/globals.css`:
- Around line 931-953: The rules using .cl-pricingTableCard:first-child and
.cl-pricingTableCard:first-child .cl-pricingTableCardContent::after incorrectly
assume the active plan is always the first child; instead inspect the
Clerk-rendered DOM for the actual active state selector (e.g., an active class
or data-attribute) and replace :first-child with that state selector across the
three places (.cl-pricingTableCard:first-child border rule, the badge/Badge
targeting if needed, and the ::after "Your Current Plan" rule) so the accent
border, badge styling, and the pseudo-element are applied only to the real
active plan element.
In `@components/Search.tsx`:
- Around line 13-29: The effect that updates the URL should use router.replace
instead of router.push, read params from searchParams.toString() (not
window.location.search), and preserve adding/removing the 'query' param based on
the query state; update the useEffect that currently references query, pathname,
router to call router.replace(`${pathname}?${params.toString()}`, { scroll:
false }). Also add a new effect that watches searchParams (from useSearchParams)
and calls setQuery(searchParams.get('query') || '') to resync the input state
when the URL changes so back/forward navigation keeps the input in sync.
In `@components/ui/button.tsx`:
- Around line 41-60: The Button component currently lets native <button> default
to submit; update the Button function to extract a "type" prop from props (e.g.,
const { type = "button", ...rest } = props) and when Comp is the native "button"
pass that type prop (allowing explicit overrides) while spreading the remaining
props; ensure Slot.Root usage (when asChild is true) still receives the original
props without forcing type, and update references to props in the return to use
the extracted/rest variables so buttonVariants and className usage remains
unchanged.
In `@components/uploadForm.tsx`:
- Around line 73-99: The current flow uploads the PDF and cover via upload(...)
(uploadedPdfBlob, uploadedCoverBlob) before downstream operations createBook()
and saveBookSegments(), which can leave public blobs orphaned on failure; change
the flow to either (A) defer calling upload(...) until after createBook() and
saveBookSegments() succeed, or (B) if you must upload first, upload to a
temporary/private key/visibility and then promote on success, or ensure robust
cleanup by wrapping createBook() and saveBookSegments() in a try/catch and
calling your blob deletion API to remove uploadedPdfBlob.url and
uploadedCoverBlob.url on any failure (including auth/quota/book-exists/segment
errors) so no public orphaned blobs remain; update the code paths that reference
upload(...), uploadedPdfBlob, uploadedCoverBlob, createBook(), and
saveBookSegments() accordingly.
In `@components/VapiControls.tsx`:
- Around line 66-76: The mic toggle button in VapiControls.tsx is icon-only and
needs an accessible name: update the <button> (the element that uses isActive,
start, stop and status) to provide a dynamic aria-label (e.g., reflect isActive:
"Stop conversation" vs "Start conversation" or similar) and consider adding
aria-pressed={isActive} for stateful semantics; ensure the label text clearly
indicates the action the button performs and ties to the existing isActive
variable so screen readers get the correct message.
In `@database/mongoose.ts`:
- Line 5: The module currently throws an Error at import-time when MONGODB_URI
is missing (the top-level check for MONGODB_URI), which can break unrelated
imports; move that validation into the connectToDatabase() function so the check
runs only when a DB connection is attempted. Remove the top-level throw for
MONGODB_URI and any other environment-dependent checks found in the same module
(lines covering the current connection setup, e.g., the block around the
existing connect/disconnect logic), and add a guard at the start of
connectToDatabase() that reads MONGODB_URI and throws a clear Error if absent;
ensure any code paths that used the old top-level variables now read the env
inside connectToDatabase() or through parameters so import-time side effects are
eliminated.
In `@lib/actions/book.actions.ts`:
- Around line 205-211: The regex fallback builds an empty pattern when
short/whitespace-only queries produce zero keywords, causing BookSegment.find({
content: { $regex: '' } }) to match everything; update the branch around
segments/keywords/pattern in the same function so that if keywords.length === 0
you either return an empty segments result immediately or set pattern to
escapeRegex(query.trim()) only when non-empty (i.e., guard before calling
BookSegment.find); refer to variables/functions BookSegment.find, segments,
keywords, pattern, escapeRegex, bookObjectId and query to locate and fix the
logic.
- Around line 42-48: Both checkBookExists and createBook currently query
Book.findOne by slug only; update them to scope lookups to the current user's
clerkId (e.g., include { slug, clerkId } in Book.findOne) and/or change their
signatures to accept a clerkId parameter derived from the request/session;
ensure createBook does not return an existing book before performing the
authorization check (perform authorization using clerkId first), and update any
calls (like uploadForm) to pass the user's clerkId so users cannot access other
users' books when titles collide.
- Around line 149-159: Ensure saveBookSegments performs server-side ownership
checks: retrieve the authenticated user via `@clerk/nextjs/server` (e.g.,
getAuth/currentUser) instead of trusting the clerkId parameter, call
connectToDatabase(), load the target Book (use the Book model) and verify its
clerkId matches the authenticated user's id (or find the book with
Book.findOne({ _id: bookId, clerkId: authUser.id })), reject/throw with a 403 or
error if not owned, and then use the authenticated clerk id when constructing
segmentsToInsert before calling BookSegment.insertMany; also remove reliance on
the client-supplied clerkId parameter in saveBookSegments.
In `@lib/constants.ts`:
- Line 96: The current export ASSISTANT_ID uses a non-null assertion which can
cause obscure runtime failures; replace it with a validated read that throws a
clear error at startup if process.env.NEXT_PUBLIC_ASSISTANT_ID is missing.
Locate the ASSISTANT_ID export and change the implementation to check the
environment variable and throw a descriptive Error (or call a shared validateEnv
helper) so the app fails fast with a clear message instead of using the "!"
assertion.
- Around line 157-167: Update Tailwind important modifiers from v3 prefix syntax
to v4 suffix syntax for the listed constants: replace leading '!' in values for
headerTitle, headerSubtitle, socialButtonsBlockButton,
socialButtonsBlockButtonText, formButtonPrimary, formFieldInput, formFieldLabel,
and footerActionLink with trailing '!' on the corresponding utility classes
(e.g., change '!text-2xl' → 'text-2xl!'; '!mt-3 !text-sm' → 'mt-3! text-sm!';
'!border' → 'border!' etc.). Ensure combined utilities preserve spacing and
other tokens (colors, hover, shadow) and do the same migration for any
`!important` usages in app/globals.css by converting applicable Tailwind
utilities to the suffix form or removing raw !important in favor of the suffix
syntax.
In `@lib/subscription-constants.ts`:
- Around line 37-40: getCurrentBillingPeriodStart currently builds the
month-start in server-local time; change it to compute the start using UTC so
billing boundaries are deterministic across regions/DST—use now.getUTCFullYear()
and now.getUTCMonth() (or Date.UTC(year, month, 1)) and return new Date(...)
created from that UTC timestamp in the getCurrentBillingPeriodStart function.
---
Minor comments:
In `@app/globals.css`:
- Around line 974-993: Remove or normalize the commented-out Clerk CSS block so
stylelint no longer flags comment-whitespace-inside: either delete the entire
block shown (the .clerk-pricing-container :deep(.cl-pricing-table),
:deep(.cl-pricing-table-header), and :deep(.cl-pricing-plan-card) rules and
their hover rule) or rewrite it as valid CSS comments using standard /* ... */
syntax with proper spacing (e.g., start with "/* " and end with " */") and no
nested comment markers like "/*!*" or "*!*/", ensuring selectors like
.clerk-pricing-container :deep(.cl-pricing-table) retain correct formatting if
kept.
In `@components/LoadingOverlay.tsx`:
- Around line 8-15: The overlay is currently visual-only; update LoadingOverlay
(around the loading-wrapper / loading-shadow / loading-title elements and the
Loader2 usage) to expose status semantics by adding role="status" and
aria-live="polite" (and aria-busy="true" while processing) to the container, and
include an offscreen text node (e.g., visually-hidden / sr-only text) that
echoes the loading message ("Synthesizing Your Book…") so screen readers
announce progress; ensure to remove or set aria-busy="false" when loading
completes.
In `@components/Navbar.tsx`:
- Around line 23-24: The alt text on the logo Image is misspelled ("Bookfied")
causing incorrect screen-reader output; update the Image component's alt prop in
Navbar.tsx (the <Image ... alt="Bookfied" ... /> instance) to the correct brand
string "Bookified" so it matches the visible .logo-text and improves
accessibility.
In `@components/ui/form.tsx`:
- Around line 28-30: Change the contexts to be nullable and update the runtime
guards: replace the current createContext casts (e.g., FormFieldContext =
React.createContext<FormFieldContextValue>({} as FormFieldContextValue)) with
nullable defaults such as React.createContext<FormFieldContextValue |
null>(null) for both FormFieldContext and the sibling context, then update all
consumers that call useContext (the places with the current if (!fieldContext)
check) to accept possibly null types and keep the explicit guard (if
(!fieldContext) throw new Error(...) or return) so the guard actually triggers;
ensure function/component signatures (types) that destructure context values are
adjusted to handle the nullable return from useContext until the guard validates
non-null.
In `@components/ui/sonner.tsx`:
- Around line 13-40: app/layout.tsx currently imports Toaster directly from the
third-party "sonner" package, bypassing the custom Toaster wrapper (the Toaster
component defined in components/ui/sonner) and losing theme, icons, and style
overrides; update the import in app/layout.tsx to import the Toaster exported by
the custom wrapper module (the Toaster component shown in the diff) instead of
from "sonner" so the app uses the wrapped Toaster with theme integration and
custom icons/styles.
In `@lib/actions/session.actions.ts`:
- Line 6: Remove the unused top-level import of getCurrentBillingPeriodStart
from the module so it’s only imported dynamically inside the function where it’s
actually used; locate the top-level import statement for
getCurrentBillingPeriodStart in session.actions.ts and delete it, leaving the
dynamic import (the one at line 14) as the sole import to avoid shadowing and
confusion.
- Around line 25-28: The if-block that checks sessionCount >=
limits.maxSessionsPerMonth should not import or call revalidatePath — remove the
dynamic import of "next/cache" and the revalidatePath("/") call inside that
branch (located around the check using sessionCount and
limits.maxSessionsPerMonth) so no unnecessary revalidation occurs when only a
billing limit is reached; ensure you only keep the limit check and any
user-facing handling (e.g., throwing or returning the error) without triggering
cache revalidation.
In `@lib/utils.ts`:
- Around line 87-91: formatDuration currently uses the raw seconds value which
allows fractional seconds to leak into the seconds portion (e.g., 91.5 ->
"1:31.5"); change the implementation in formatDuration to first normalize to an
integer (e.g., const total = Math.floor(seconds)) and then compute mins =
Math.floor(total / 60) and secs = total % 60 so the output is always MM:SS with
seconds zero-padded via padStart on secs.toString().
In `@types.d.ts`:
- Line 5: The current default import "import z from 'zod'" in types.d.ts should
be changed to the named import used elsewhere: replace it with "import { z }
from 'zod'" so that the file uses the same Zod symbol as the rest of the
codebase (see usages of z and the pattern in lib/zod.ts); update the import
statement only — no other code changes required.
---
Nitpick comments:
In `@app/`(root)/page.tsx:
- Line 1: The file contains an unnecessary top-level import "import React from
\"react\"" which is not required with the React 19/new JSX transform; remove
that import line and ensure no other references to the React symbol remain in
the module (e.g., check for direct uses of React.*); if any runtime types are
needed, import specific symbols instead of the default React import.
In `@components/FileUploader.tsx`:
- Line 47: The current check uses const isUploaded = !!value and later casts
value with (value as File), which can crash if value is a truthy non-File; add a
proper type guard before treating it as a File (e.g., implement an
isFile(value): value is File that checks value instanceof File and use it
wherever you currently use isUploaded or (value as File)), update the code paths
that rely on (value as File) to first call isFile(value) and handle non-File
truthy values with a safe fallback (e.g., treat as not uploaded or skip
file-specific logic) so all uses of value are type-safe.
In `@components/HeroSection.tsx`:
- Around line 47-69: Replace the non-semantic unordered list with an ordered
list for the step sequence: change the <ul className="space-y-6"> wrapper to an
<ol> and keep the existing <li className="library-step-item"> children and their
inner elements (the numbered div, h3 with class library-step-title, and p with
class library-step-description); also ensure any CSS that targets ul (e.g.,
spacing or list-style) is adjusted to support <ol> (add list-style or
list-decimal as needed) so visual numbering and spacing remain correct in the
HeroSection component.
In `@components/Transcript.tsx`:
- Around line 44-45: The transcript container div currently referenced by
scrollRef and rendering messages should be marked as a live log region for
screen readers; update the div (the one with ref={scrollRef} and
className="transcript-messages ...") to include accessibility attributes such as
role="log", aria-live="polite" (or "assertive" if immediate interruption is
desired), and aria-atomic="false" so assistive tech announces new appended
messages appropriately.
In `@database/models/book.model.ts`:
- Around line 4-16: Add a single-field index on clerkId to the BookSchema to
speed up queries that filter by user; update the schema definition (BookSchema)
by adding an index for the clerkId field (e.g., using schema.index({ clerkId: 1
}) or the equivalent within the Schema options) so queries against IBook
documents by clerkId are covered efficiently while keeping the existing unique
index on slug intact.
In `@database/models/voice-session.model.ts`:
- Line 5: Remove the redundant single-field index on clerkId: the field
definition clerkId: { type: String, required: true, index: true } creates an
individual index that is already covered by the compound index { clerkId: 1,
billingPeriodStart: 1 } defined elsewhere in the model; update the clerkId field
to remove index: true so only the compound index (the one that includes clerkId
and billingPeriodStart) remains to avoid duplicate index overhead.
In `@lib/actions/session.actions.ts`:
- Around line 55-62: The endVoiceSession function accepts durationSeconds
without validation; add input validation before calling
VoiceSession.findByIdAndUpdate to reject or clamp invalid values (e.g., ensure
durationSeconds is a finite number, >= 0 and <= a sensible max like several
hours or a configured MAX_SESSION_DURATION_SECONDS), and throw or return a clear
error (or normalize the value) if it fails validation; update endVoiceSession to
validate durationSeconds and only proceed with the DB update when the value
passes checks.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ccad900c-c349-408e-9e41-f87cc676bb1e
⛔ Files ignored due to path filters (16)
app/favicon.icois excluded by!**/*.icopackage-lock.jsonis excluded by!**/package-lock.jsonpublic/assets/Gemini_Generated_Image_jlix6fjlix6fjlix (1) 1.pngis excluded by!**/*.pngpublic/assets/banner.svgis excluded by!**/*.svgpublic/assets/book-cover.svgis excluded by!**/*.svgpublic/assets/book.pngis excluded by!**/*.pngpublic/assets/desktop-banner.pngis excluded by!**/*.pngpublic/assets/hero-illustration.pngis excluded by!**/*.pngpublic/assets/image.pngis excluded by!**/*.pngpublic/assets/loader.pngis excluded by!**/*.pngpublic/assets/logo-full.svgis excluded by!**/*.svgpublic/assets/logo.pngis excluded by!**/*.pngpublic/assets/mobile-banner.pngis excluded by!**/*.pngpublic/assets/open-book.svgis excluded by!**/*.svgpublic/assets/tab-banner.pngis excluded by!**/*.pngpublic/assets/wave.pngis excluded by!**/*.png
📒 Files selected for processing (42)
.gitignoreAGENTS.mdCLAUDE.mdREADME.mdapp/(root)/books/new/page.tsxapp/(root)/page.tsxapp/globals.cssapp/layout.tsxcomponents.jsoncomponents/BookCard.tsxcomponents/FileUploader.tsxcomponents/HeroSection.tsxcomponents/LoadingOverlay.tsxcomponents/Navbar.tsxcomponents/Search.tsxcomponents/Transcript.tsxcomponents/VapiControls.tsxcomponents/VoiceSelector.tsxcomponents/ui/button.tsxcomponents/ui/form.tsxcomponents/ui/input.tsxcomponents/ui/label.tsxcomponents/ui/radio-group.tsxcomponents/ui/sonner.tsxcomponents/uploadForm.tsxdatabase/models/book-segment.model.tsdatabase/models/book.model.tsdatabase/models/voice-session.model.tsdatabase/mongoose.tseslint.config.mjslib/actions/book.actions.tslib/actions/session.actions.tslib/constants.tslib/subscription-constants.tslib/subscription.server.tslib/utils.tslib/zod.tsnext.config.tspackage.jsonpostcss.config.mjstsconfig.jsontypes.d.ts
| /* Add accent border to active plan card | ||
| Note: This uses :first-child which assumes active plan is always first. | ||
| If Clerk changes the order, this may highlight the wrong card. */ | ||
| .cl-pricingTableCard:first-child { | ||
| border: 2px solid #663820 !important; | ||
| } | ||
|
|
||
| /* Style the Active badge with better contrast */ | ||
| .cl-badge, | ||
| .cl-pricingTableCard [class*="badge"], | ||
| .cl-pricingTableCard span[class*="Badge"] { | ||
| background-color: #663820 !important; | ||
| color: white !important; | ||
| font-weight: 600 !important; | ||
| padding: 0.25rem 0.75rem !important; | ||
| border-radius: 9999px !important; | ||
| } | ||
|
|
||
| /* Add "Current Plan" indicator to active plan that doesn't have a button | ||
| Note: Hardcoded English text - if i18n support is needed, this should be | ||
| handled in JavaScript with translated strings rendered in the DOM. */ | ||
| .cl-pricingTableCard:first-child .cl-pricingTableCardContent::after { | ||
| content: "Your Current Plan"; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's check the full CSS file around the lines mentioned
wc -l app/globals.cssRepository: Jingli-123/bookified
Length of output: 84
🏁 Script executed:
# Look at the CSS context more broadly
sed -n '920,960p' app/globals.cssRepository: Jingli-123/bookified
Length of output: 1474
🏁 Script executed:
# Search for any JavaScript that handles Clerk pricing or active plan logic
rg -t ts -t tsx -t js -t jsx "pricingTableCard|Clerk|pricing" --max-count 20Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
# Look for any data attributes or classes that might mark the active plan
rg -t ts -t tsx -t js -t jsx "active|current.*plan|isPricingCard" --max-count 20Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
# Check if there's any Clerk configuration or integration code
fd -t f "clerk" --iglobRepository: Jingli-123/bookified
Length of output: 267
🏁 Script executed:
# Search for JavaScript/TypeScript files
fd -e js -e ts | head -20Repository: Jingli-123/bookified
Length of output: 375
🏁 Script executed:
# Look for files containing Clerk or pricing references
rg "Clerk|pricing" --type-list | headRepository: Jingli-123/bookified
Length of output: 454
🏁 Script executed:
# Search for any files containing "pricingTableCard"
rg "pricingTableCard" -lRepository: Jingli-123/bookified
Length of output: 80
🏁 Script executed:
# Search for Clerk imports or pricing component usage
rg "from.*clerk|import.*clerk|@clerk" -iRepository: Jingli-123/bookified
Length of output: 1871
🏁 Script executed:
# Look for any data attributes or aria attributes that might mark active
rg "data-|aria-" app/globals.cssRepository: Jingli-123/bookified
Length of output: 398
🏁 Script executed:
# Look for pricing or subscription related components
fd -e tsx -e jsx | xargs rg -l "pricing|subscription|plan" 2>/dev/null | grep -i "component\|page"Repository: Jingli-123/bookified
Length of output: 140
🏁 Script executed:
# Search for where the pricing table is actually rendered
rg "cl-pricingTableCard|pricing.*table|Clerk.*pricing" -iRepository: Jingli-123/bookified
Length of output: 1603
🏁 Script executed:
# Look at the structure of app directory to find pricing page/component
find app -name "*pricing*" -o -name "*subscription*" -o -name "*plan*"Repository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check if there are any other CSS classes related to "active" or "current"
rg "\.cl-.*active|\.cl-.*current" app/globals.cssRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Look for any Clerk documentation in the repo or check component imports
rg "from.*clerk|import.*[Pp]ricing" --max-count 15Repository: Jingli-123/bookified
Length of output: 380
🏁 Script executed:
# Search for any JSX/component files that might contain pricing table
rg "PricingTable|<Pricing|pricing-table" -iRepository: Jingli-123/bookified
Length of output: 1402
🏁 Script executed:
# Look at the commented CSS to understand original structure
sed -n '960,1000p' app/globals.cssRepository: Jingli-123/bookified
Length of output: 1018
🏁 Script executed:
# Get the full pricing-related CSS to understand the structure
rg "\.clerk-pricing" -A 5 app/globals.css | head -60Repository: Jingli-123/bookified
Length of output: 1684
🏁 Script executed:
# Search all files for any element or import that might use pricing
rg "clerk.*Pricing|PricingTable" -i --max-count 20Repository: Jingli-123/bookified
Length of output: 1251
🏁 Script executed:
# Check if there's a pricing page anywhere
find app -type f -name "*.tsx" -o -name "*.jsx" | xargs grep -l "pricing\|subscription.*plan" 2>/dev/nullRepository: Jingli-123/bookified
Length of output: 48
🏁 Script executed:
# Look for any data attributes or class markers that Clerk uses for active plans
rg "data-active|data-current|is-active|is-current|active-plan" -iRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check if there's any documentation or comments about Clerk pricing
rg "clerk.*pricing\|pricing.*plan\|active.*plan" -i -B 2 -A 2Repository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Look for the full .cl-pricingTableCard definition to see all selectors
rg "\.cl-pricingTableCard" app/globals.css -B 2 -A 2Repository: Jingli-123/bookified
Length of output: 1047
🏁 Script executed:
# Search for any attributes that Clerk components use (aria-current, data-state, etc)
rg "aria-current|data-state|data-selected" -iRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check if the pricing table is actually rendered anywhere - search for Clerk components
find . -name "*.tsx" -o -name "*.jsx" | head -30 | xargs grep -l "clerk\|Clerk" 2>/dev/null | xargs grep -l "pricing\|Pricing" 2>/dev/nullRepository: Jingli-123/bookified
Length of output: 88
🏁 Script executed:
# Let's check if there's a pricing page that might not have been found
find . -type f \( -name "*.tsx" -o -name "*.jsx" -o -name "*.html" \) | xargs grep -l "clerk-pricing\|cl-pricingTableCard\|PricingTable" 2>/dev/nullRepository: Jingli-123/bookified
Length of output: 48
🏁 Script executed:
# Check app directory structure
ls -la app/Repository: Jingli-123/bookified
Length of output: 420
🏁 Script executed:
# Look for any route or page that might have pricing
find app -type f -name "*.tsx" | head -20Repository: Jingli-123/bookified
Length of output: 129
🏁 Script executed:
# Investigate the Navbar.tsx to see if it contains pricing
cat components/Navbar.tsx | head -50Repository: Jingli-123/bookified
Length of output: 2171
🏁 Script executed:
# Search for web_search on Clerk pricing table to understand how it marks active plans
# First, let me check Clerk documentation about how they mark active subscriptionsRepository: Jingli-123/bookified
Length of output: 46
🌐 Web query:
Clerk pricing table active plan CSS class selector
💡 Result:
Clerk's <PricingTable /> component renders a pricing table for billing plans, but official documentation does not explicitly list specific CSS class selectors for the "active plan" [1][2][3][4]. Clerk components use stable CSS classes prefixed with 'cl-' (e.g., cl-formButtonPrimary, cl-button), which are safe to target for custom styling [5][6][7]. To find the exact selector for the active plan (likely highlighting the user's current subscription), inspect the rendered component in the browser dev tools, as classes follow the 'cl-' pattern and are documented as inspectable [6]. Alternatively, customize via the appearance prop's elements object (remove 'cl-' prefix as key) or variables for theming without direct selectors [1][8]. No public list of PricingTable-specific classes like 'cl-planActive' was found in docs as of 2026-05-06.
Citations:
- 1: https://clerk.com/docs/react/reference/components/billing/pricing-table
- 2: https://clerk.com/docs/js-frontend/components/billing/pricing-table
- 3: https://clerk.com/docs/nextjs/reference/components/billing/pricing-table
- 4: https://clerk.com/docs/astro/components/billing/pricing-table
- 5: https://clerk.com/docs/vue/guides/customizing-clerk/appearance-prop/bring-your-own-css
- 6: https://clerk.com/docs/go/guides/customizing-clerk/appearance-prop/bring-your-own-css
- 7: https://clerk.com/docs/nuxt/guides/customizing-clerk/appearance-prop/bring-your-own-css
- 8: https://clerk.com/docs/js-frontend/guides/customizing-clerk/appearance-prop/overview
🏁 Script executed:
# Look for the subscriptions page mentioned in Navbar
find . -path "*/subscriptions*" -type fRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check app structure more carefully
find app -type d | head -20Repository: Jingli-123/bookified
Length of output: 117
🏁 Script executed:
# Look for the subscriptions route
find . -name "*subscriptions*" -o -name "*pricing*" 2>/dev/nullRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check if there's any configuration or setup file mentioning Clerk pricing
rg "PricingTable\|pricing" -i package.jsonRepository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# One more check - see if there are any console logs or comments about the pricing implementation
rg "TODO|FIXME|NOTE" app/globals.css | grep -i "pric\|clerk\|active"Repository: Jingli-123/bookified
Length of output: 46
Use a state-based selector for the active plan, not :first-child.
The :first-child selector marks whichever plan card renders first as the current plan. If Clerk changes plan order or reorders them dynamically, the wrong tier gets the accent border and "Your Current Plan" badge. Inspect the rendered component in browser dev tools to find the actual CSS class or attribute Clerk applies to the active subscription, then use that selector instead.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@app/globals.css` around lines 931 - 953, The rules using
.cl-pricingTableCard:first-child and .cl-pricingTableCard:first-child
.cl-pricingTableCardContent::after incorrectly assume the active plan is always
the first child; instead inspect the Clerk-rendered DOM for the actual active
state selector (e.g., an active class or data-attribute) and replace
:first-child with that state selector across the three places
(.cl-pricingTableCard:first-child border rule, the badge/Badge targeting if
needed, and the ::after "Your Current Plan" rule) so the accent border, badge
styling, and the pseudo-element are applied only to the real active plan
element.
| const [query, setQuery] = useState(searchParams.get('query') || ''); | ||
|
|
||
| useEffect(() => { | ||
| const delayDebounceFn = setTimeout(() => { | ||
| const params = new URLSearchParams(window.location.search); | ||
|
|
||
| if (query) { | ||
| params.set('query', query); | ||
| } else { | ||
| params.delete('query'); | ||
| } | ||
|
|
||
| router.push(`${pathname}?${params.toString()}`, { scroll: false }); | ||
| }, 300); | ||
|
|
||
| return () => clearTimeout(delayDebounceFn); | ||
| }, [query, pathname, router]); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Search for Next.js documentation on router APIs in node_modules
find node_modules/next -name "*.md" -o -name "*.json" | head -20Repository: Jingli-123/bookified
Length of output: 1201
🌐 Web query:
Next.js router.push vs router.replace API 2025 2026
💡 Result:
In Next.js (2025/2026), router.push and router.replace do the same kind of client-side navigation API, but differ in how they affect the browser history. 1) router.push - Behavior: navigates to the new route and adds a new entry to the browser history stack, so the user can use the Back button to return to the previous page/state. [1][2] - App Router (next/navigation): useRouter.push(href, options) “adds a new entry into the browser’s history stack.” [2] - Pages Router (next/router): router.push is used for client-side transitions; the docs describe router.replace as analogous and note push adds a new history entry (and that both have the same API shape for arguments/options). [1][3] 2) router.replace - Behavior: navigates to the new route but replaces the current history entry (no new Back step to the replaced page). [1][2][3] - App Router: useRouter.replace(href, options) “without adding a new entry into the browser’s history stack.” [2] - Pages Router: router.replace “will prevent adding a new URL entry into the history stack,” and “the API for router.replace is exactly the same as the API for router.push.” [3] 3) Practical “when to use which” - Use router.push for normal in-app navigation where Back should take the user to the prior screen/state. [1][2] - Use router.replace for redirects / one-way flows (e.g., after login/logout, onboarding step transitions) where you do not want the user to go “Back” to the previous step. [1] 4) Notes about API compatibility across routers (important for 2025/2026) - App Router: import useRouter from next/navigation; then call router.push/router.replace from that hook. [2] - Pages Router: router.push/router.replace come from next/router’s useRouter. [1][3] Answer summary: router.push adds a history entry (Back returns), while router.replace swaps the current history entry (Back won’t return to the replaced page). [2][3]
Citations:
- 1: https://nextjs.org/docs/pages/api-reference/functions/use-router
- 2: https://nextjs.org/docs/app/api-reference/functions/use-router
- 3: https://nextjs.org/docs/api-reference/next/router
🏁 Script executed:
# Check if the file exists and examine its structure to understand the full context
find . -name "Search.tsx" -type f 2>/dev/nullRepository: Jingli-123/bookified
Length of output: 88
🏁 Script executed:
# Check the Next.js version in use
if [ -f "package.json" ]; then
grep -A2 '"next"' package.json
fiRepository: Jingli-123/bookified
Length of output: 145
🏁 Script executed:
cat -n components/Search.tsxRepository: Jingli-123/bookified
Length of output: 1942
🏁 Script executed:
# Check if searchParams is imported from next/navigation
head -20 components/Search.tsxRepository: Jingli-123/bookified
Length of output: 724
Use router.replace and resync from the URL.
router.push creates a browser-history entry for every debounced keystroke, and query is only initialized once from useSearchParams(), so back/forward navigation leaves the input out of sync with the actual URL. Replace router.push with router.replace, use searchParams.toString() instead of window.location.search, and add a separate effect to mirror searchParams back into state when it changes.
Suggested fix
const [query, setQuery] = useState(searchParams.get('query') || '');
+
+useEffect(() => {
+ setQuery(searchParams.get('query') || '');
+}, [searchParams]);
useEffect(() => {
const delayDebounceFn = setTimeout(() => {
- const params = new URLSearchParams(window.location.search);
+ const params = new URLSearchParams(searchParams.toString());
if (query) {
params.set('query', query);
} else {
params.delete('query');
}
- router.push(`${pathname}?${params.toString()}`, { scroll: false });
+ const next = params.toString();
+ router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false });
}, 300);
return () => clearTimeout(delayDebounceFn);
-}, [query, pathname, router]);
+}, [query, pathname, router, searchParams]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const [query, setQuery] = useState(searchParams.get('query') || ''); | |
| useEffect(() => { | |
| const delayDebounceFn = setTimeout(() => { | |
| const params = new URLSearchParams(window.location.search); | |
| if (query) { | |
| params.set('query', query); | |
| } else { | |
| params.delete('query'); | |
| } | |
| router.push(`${pathname}?${params.toString()}`, { scroll: false }); | |
| }, 300); | |
| return () => clearTimeout(delayDebounceFn); | |
| }, [query, pathname, router]); | |
| const [query, setQuery] = useState(searchParams.get('query') || ''); | |
| useEffect(() => { | |
| setQuery(searchParams.get('query') || ''); | |
| }, [searchParams]); | |
| useEffect(() => { | |
| const delayDebounceFn = setTimeout(() => { | |
| const params = new URLSearchParams(searchParams.toString()); | |
| if (query) { | |
| params.set('query', query); | |
| } else { | |
| params.delete('query'); | |
| } | |
| const next = params.toString(); | |
| router.replace(next ? `${pathname}?${next}` : pathname, { scroll: false }); | |
| }, 300); | |
| return () => clearTimeout(delayDebounceFn); | |
| }, [query, pathname, router, searchParams]); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/Search.tsx` around lines 13 - 29, The effect that updates the URL
should use router.replace instead of router.push, read params from
searchParams.toString() (not window.location.search), and preserve
adding/removing the 'query' param based on the query state; update the useEffect
that currently references query, pathname, router to call
router.replace(`${pathname}?${params.toString()}`, { scroll: false }). Also add
a new effect that watches searchParams (from useSearchParams) and calls
setQuery(searchParams.get('query') || '') to resync the input state when the URL
changes so back/forward navigation keeps the input in sync.
| function Button({ | ||
| className, | ||
| variant = "default", | ||
| size = "default", | ||
| asChild = false, | ||
| ...props | ||
| }: React.ComponentProps<"button"> & | ||
| VariantProps<typeof buttonVariants> & { | ||
| asChild?: boolean | ||
| }) { | ||
| const Comp = asChild ? Slot.Root : "button" | ||
|
|
||
| return ( | ||
| <Comp | ||
| data-slot="button" | ||
| data-variant={variant} | ||
| data-size={size} | ||
| className={cn(buttonVariants({ variant, size, className }))} | ||
| {...props} | ||
| /> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git ls-files | grep -E "(button|Button)" | head -20Repository: Jingli-123/bookified
Length of output: 89
🏁 Script executed:
cat -n components/ui/button.tsxRepository: Jingli-123/bookified
Length of output: 2904
Default native buttons to type="button" to prevent unexpected form submissions.
When asChild is false and callers omit type, the HTML button element defaults to "submit" within form contexts, causing secondary actions inside forms to submit unexpectedly. Extracting the type prop and defaulting to "button" prevents this behavior while allowing explicit type overrides when needed.
Suggested fix
function Button({
className,
variant = "default",
size = "default",
asChild = false,
+ type,
...props
}: React.ComponentProps<"button"> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean
}) {
const Comp = asChild ? Slot.Root : "button"
return (
<Comp
+ type={asChild ? undefined : type ?? "button"}
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/ui/button.tsx` around lines 41 - 60, The Button component
currently lets native <button> default to submit; update the Button function to
extract a "type" prop from props (e.g., const { type = "button", ...rest } =
props) and when Comp is the native "button" pass that type prop (allowing
explicit overrides) while spreading the remaining props; ensure Slot.Root usage
(when asChild is true) still receives the original props without forcing type,
and update references to props in the return to use the extracted/rest variables
so buttonVariants and className usage remains unchanged.
| const uploadedPdfBlob = await upload(fileTitle, pdfFile, { | ||
| access: 'public', | ||
| handleUploadUrl: '/api/upload', | ||
| contentType: 'application/pdf' | ||
| }); | ||
|
|
||
| let coverUrl: string; | ||
|
|
||
| if(data.coverImage) { | ||
| const coverFile = data.coverImage; | ||
| const uploadedCoverBlob = await upload(`${fileTitle}_cover.png`, coverFile, { | ||
| access: 'public', | ||
| handleUploadUrl: '/api/upload', | ||
| contentType: coverFile.type | ||
| }); | ||
| coverUrl = uploadedCoverBlob.url; | ||
| } else { | ||
| const response = await fetch(parsedPDF.cover) | ||
| const blob = await response.blob(); | ||
|
|
||
| const uploadedCoverBlob = await upload(`${fileTitle}_cover.png`, blob, { | ||
| access: 'public', | ||
| handleUploadUrl: '/api/upload', | ||
| contentType: 'image/png' | ||
| }); | ||
| coverUrl = uploadedCoverBlob.url; | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, locate the uploadForm.tsx file
fd -t f "uploadForm.tsx"Repository: Jingli-123/bookified
Length of output: 90
🏁 Script executed:
# Get the file size to determine how to read it
wc -l components/uploadForm.tsxRepository: Jingli-123/bookified
Length of output: 94
🏁 Script executed:
# Read the entire file to understand the full upload flow
cat -n components/uploadForm.tsxRepository: Jingli-123/bookified
Length of output: 11426
🏁 Script executed:
# Search for blob deletion or cleanup patterns in the codebase
rg "delete|remove|cleanup" --type ts --type tsx --type js --type jsx -i | head -30Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
# Search specifically for vercel blob deletion API usage
rg "blob" --type ts --type tsx | grep -i "delete\|remove\|clean" | head -20Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
# Check for any error handling or transaction patterns around uploads
rg "catch|error|finally" components/uploadForm.tsx -A 2 -B 2Repository: Jingli-123/bookified
Length of output: 1054
🏁 Script executed:
# Search for any reference to blob cleanup or rollback in the entire codebase
fd -t f -e ts -e tsx -e js -e jsx | xargs rg "deleteBlob|deleteFile|removeBlob|cleanupBlob|rollback" 2>/dev/null | head -20Repository: Jingli-123/bookified
Length of output: 46
🏁 Script executed:
# Check if there's any blob deletion API or service in the project
fd -t f "\.ts$" -o -t f "\.tsx$" -o -t f "\.js$" | xargs rg "delete" | grep -i "blob\|file\|storage" | head -20Repository: Jingli-123/bookified
Length of output: 181
🏁 Script executed:
# Look for book.actions or related service files that might handle cleanup
fd -t f "book\.actions" -o -t f "blob" -o -t f "upload"Repository: Jingli-123/bookified
Length of output: 181
🏁 Script executed:
# Check what happens in createBook and saveBookSegments when they fail
fd -t f "book\.actions\."Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
# Read the book.actions.ts file to see createBook and saveBookSegments implementation
wc -l lib/actions/book.actions.tsRepository: Jingli-123/bookified
Length of output: 96
🏁 Script executed:
# Read the full book.actions.ts file
cat -n lib/actions/book.actions.tsRepository: Jingli-123/bookified
Length of output: 8472
Blob uploads can leak on downstream failures.
Both the PDF and cover are uploaded to public blob storage (lines 73-98) before createBook() (line 101) and saveBookSegments() (line 127) complete. If authorization fails (line 91-93), quota limits are exceeded (line 100), the book already exists (line 76-81), or segment persistence fails (line 169-176), the app leaves public blobs orphaned with no cleanup or owning database record.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/uploadForm.tsx` around lines 73 - 99, The current flow uploads the
PDF and cover via upload(...) (uploadedPdfBlob, uploadedCoverBlob) before
downstream operations createBook() and saveBookSegments(), which can leave
public blobs orphaned on failure; change the flow to either (A) defer calling
upload(...) until after createBook() and saveBookSegments() succeed, or (B) if
you must upload first, upload to a temporary/private key/visibility and then
promote on success, or ensure robust cleanup by wrapping createBook() and
saveBookSegments() in a try/catch and calling your blob deletion API to remove
uploadedPdfBlob.url and uploadedCoverBlob.url on any failure (including
auth/quota/book-exists/segment errors) so no public orphaned blobs remain;
update the code paths that reference upload(...), uploadedPdfBlob,
uploadedCoverBlob, createBook(), and saveBookSegments() accordingly.
| <button | ||
| onClick={isActive ? stop : start} | ||
| disabled={status === 'connecting'} | ||
| className={`vapi-mic-btn shadow-md !w-[60px] !h-[60px] z-10 ${isActive ? 'vapi-mic-btn-active' : 'vapi-mic-btn-inactive'}`} | ||
| > | ||
| {isActive ? ( | ||
| <Mic className="size-7 text-white" /> | ||
| ) : ( | ||
| <MicOff className="size-7 text-[#212a3b]" /> | ||
| )} | ||
| </button> |
There was a problem hiding this comment.
Add an accessible name to the mic toggle.
This is an icon-only button. Without an aria-label, screen-reader users won't know whether the control starts or stops the conversation.
Suggested fix
<button
onClick={isActive ? stop : start}
disabled={status === 'connecting'}
+ aria-label={isActive ? 'Stop conversation' : 'Start conversation'}
className={`vapi-mic-btn shadow-md !w-[60px] !h-[60px] z-10 ${isActive ? 'vapi-mic-btn-active' : 'vapi-mic-btn-inactive'}`}
>🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@components/VapiControls.tsx` around lines 66 - 76, The mic toggle button in
VapiControls.tsx is icon-only and needs an accessible name: update the <button>
(the element that uses isActive, start, stop and status) to provide a dynamic
aria-label (e.g., reflect isActive: "Stop conversation" vs "Start conversation"
or similar) and consider adding aria-pressed={isActive} for stateful semantics;
ensure the label text clearly indicates the action the button performs and ties
to the existing isActive variable so screen readers get the correct message.
| export const saveBookSegments = async (bookId: string, clerkId: string, segments: TextSegment[]) => { | ||
| try { | ||
| await connectToDatabase(); | ||
|
|
||
| console.log('Saving book segments...'); | ||
|
|
||
| const segmentsToInsert = segments.map(({ text, segmentIndex, pageNumber, wordCount }) => ({ | ||
| clerkId, bookId, content: text, segmentIndex, pageNumber, wordCount | ||
| })); | ||
|
|
||
| await BookSegment.insertMany(segmentsToInsert); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n lib/actions/book.actions.ts | head -200Repository: Jingli-123/bookified
Length of output: 7231
🏁 Script executed:
rg "saveBookSegments" --type ts --type tsx -B 2 -A 2Repository: Jingli-123/bookified
Length of output: 92
🏁 Script executed:
rg "saveBookSegments" -B 2 -A 2Repository: Jingli-123/bookified
Length of output: 1022
🏁 Script executed:
cat -n components/uploadForm.tsx | grep -A 30 "saveBookSegments"Repository: Jingli-123/bookified
Length of output: 2426
🏁 Script executed:
ls -la node_modules/next/dist/docs/ | head -20Repository: Jingli-123/bookified
Length of output: 140
🏁 Script executed:
cat package.json | grep -A 5 -B 5 '"next"'Repository: Jingli-123/bookified
Length of output: 368
Add server-side authorization to saveBookSegments() to prevent cross-user data manipulation.
saveBookSegments() accepts bookId and clerkId from the client and writes segments immediately without verifying the authenticated user owns the target book. A malicious client can forge these values to attach segments to another user's book or corrupt its metadata. Retrieve the authenticated user from @clerk/nextjs/server and verify the book belongs to that user before inserting segments.
Suggested guard
export const saveBookSegments = async (bookId: string, clerkId: string, segments: TextSegment[]) => {
try {
await connectToDatabase();
+ const { auth } = await import("@clerk/nextjs/server");
+ const { userId } = await auth();
+
+ if (!userId || userId !== clerkId) {
+ return { success: false, error: "Unauthorized" };
+ }
+
+ const book = await Book.findOne({ _id: bookId, clerkId: userId }).lean();
+ if (!book) {
+ return { success: false, error: "Book not found" };
+ }
console.log('Saving book segments...');🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/actions/book.actions.ts` around lines 149 - 159, Ensure saveBookSegments
performs server-side ownership checks: retrieve the authenticated user via
`@clerk/nextjs/server` (e.g., getAuth/currentUser) instead of trusting the clerkId
parameter, call connectToDatabase(), load the target Book (use the Book model)
and verify its clerkId matches the authenticated user's id (or find the book
with Book.findOne({ _id: bookId, clerkId: authUser.id })), reject/throw with a
403 or error if not owned, and then use the authenticated clerk id when
constructing segmentsToInsert before calling BookSegment.insertMany; also remove
reliance on the client-supplied clerkId parameter in saveBookSegments.
| if (segments.length === 0) { | ||
| const keywords = query.split(/\s+/).filter((k) => k.length > 2); | ||
| const pattern = keywords.map(escapeRegex).join('|'); | ||
|
|
||
| segments = await BookSegment.find({ | ||
| bookId: bookObjectId, | ||
| content: { $regex: pattern, $options: 'i' }, |
There was a problem hiding this comment.
Guard the regex fallback when the query has no searchable tokens.
If the query is only short words like "AI" or whitespace, keywords becomes empty and Line 211 builds /$regex: ''/, which matches every segment in the book. Return an empty result, or fall back to the escaped raw query, before issuing the regex search.
Suggested fix
const keywords = query.split(/\s+/).filter((k) => k.length > 2);
+if (keywords.length === 0) {
+ return {
+ success: true,
+ data: [],
+ };
+}
const pattern = keywords.map(escapeRegex).join('|');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| if (segments.length === 0) { | |
| const keywords = query.split(/\s+/).filter((k) => k.length > 2); | |
| const pattern = keywords.map(escapeRegex).join('|'); | |
| segments = await BookSegment.find({ | |
| bookId: bookObjectId, | |
| content: { $regex: pattern, $options: 'i' }, | |
| if (segments.length === 0) { | |
| const keywords = query.split(/\s+/).filter((k) => k.length > 2); | |
| if (keywords.length === 0) { | |
| return { | |
| success: true, | |
| data: [], | |
| }; | |
| } | |
| const pattern = keywords.map(escapeRegex).join('|'); | |
| segments = await BookSegment.find({ | |
| bookId: bookObjectId, | |
| content: { $regex: pattern, $options: 'i' }, |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/actions/book.actions.ts` around lines 205 - 211, The regex fallback
builds an empty pattern when short/whitespace-only queries produce zero
keywords, causing BookSegment.find({ content: { $regex: '' } }) to match
everything; update the branch around segments/keywords/pattern in the same
function so that if keywords.length === 0 you either return an empty segments
result immediately or set pattern to escapeRegex(query.trim()) only when
non-empty (i.e., guard before calling BookSegment.find); refer to
variables/functions BookSegment.find, segments, keywords, pattern, escapeRegex,
bookObjectId and query to locate and fix the logic.
| export const ACCEPTED_IMAGE_TYPES = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']; | ||
|
|
||
| // Pre-configured VAPI assistant ID (hardcoded for this app) | ||
| export const ASSISTANT_ID = process.env.NEXT_PUBLIC_ASSISTANT_ID!; |
There was a problem hiding this comment.
Avoid non-null assertion for required env config.
Line 96 can produce hard-to-debug failures when the variable is missing. Fail fast with an explicit startup check.
Suggested fix
-export const ASSISTANT_ID = process.env.NEXT_PUBLIC_ASSISTANT_ID!;
+const assistantId = process.env.NEXT_PUBLIC_ASSISTANT_ID;
+if (!assistantId) {
+ throw new Error("Missing NEXT_PUBLIC_ASSISTANT_ID");
+}
+export const ASSISTANT_ID = assistantId;📝 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.
| export const ASSISTANT_ID = process.env.NEXT_PUBLIC_ASSISTANT_ID!; | |
| const assistantId = process.env.NEXT_PUBLIC_ASSISTANT_ID; | |
| if (!assistantId) { | |
| throw new Error("Missing NEXT_PUBLIC_ASSISTANT_ID"); | |
| } | |
| export const ASSISTANT_ID = assistantId; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/constants.ts` at line 96, The current export ASSISTANT_ID uses a non-null
assertion which can cause obscure runtime failures; replace it with a validated
read that throws a clear error at startup if
process.env.NEXT_PUBLIC_ASSISTANT_ID is missing. Locate the ASSISTANT_ID export
and change the implementation to check the environment variable and throw a
descriptive Error (or call a shared validateEnv helper) so the app fails fast
with a clear message instead of using the "!" assertion.
| headerTitle: '!text-2xl font-bold text-[#212a3b]', | ||
| headerSubtitle: '!mt-3 !text-sm text-[#3d485e]', | ||
| socialButtonsBlockButton: | ||
| '!border border-[rgba(33,42,59,0.12)] hover:bg-[#212a3b]/10 transition-all h-12 text-lg !rounded-xl shadow-[0_1px_3px_rgba(0,0,0,0.04),0_4px_12px_rgba(0,0,0,0.08)]', | ||
| socialButtonsBlockButtonText: 'font-medium !text-[#212a3b] !text-lg', | ||
| formButtonPrimary: | ||
| 'bg-[#212a3b] hover:bg-[#3d485e] text-white font-medium !border-0 shadow-[0_1px_3px_rgba(0,0,0,0.04),0_4px_12px_rgba(0,0,0,0.08)] normal-case !h-12 !text-lg !rounded-xl', | ||
| formFieldInput: | ||
| '!border !border-[rgba(33,42,59,0.12)] !rounded-xl focus:ring-[#212a3b] focus:border-[#212a3b] !h-12 !min-h-12 !text-lg !bg-white shadow-[0_1px_2px_rgba(0,0,0,0.04),0_1px_3px_rgba(0,0,0,0.06)]', | ||
| formFieldLabel: 'text-[#212a3b] font-medium text-lg', | ||
| footerActionLink: 'text-[#212a3b] hover:text-[#3d485e] text-base font-medium', |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== Tailwind version from package manifests =="
fd -i 'package.json' -x sh -c 'echo "--- $1"; rg -n "\"tailwindcss\"\\s*:" "$1" || true' sh {}
echo
echo "== Possible v3-style important modifiers in TS/TSX/CSS =="
rg -nP --iglob '*.{ts,tsx,js,jsx,css}' '(^|[\s"'"'"'`])![a-zA-Z0-9_-]+'Repository: Jingli-123/bookified
Length of output: 6263
🌐 Web query:
Tailwind CSS v4 important modifier syntax prefix vs suffix
💡 Result:
In Tailwind CSS v4, the important modifier (!) has changed position compared to v3. It is now a suffix placed at the very end of the class name, after any variants and the utility name. For example: flex! bg-red-500! hover:bg-red-600/50![1][2]. This generates CSS with!important declarations[2]. The old prefix position (e.g.,!bg-red-500 after variants) from v3 is still supported for backwards compatibility but is deprecated[1][3]. Official docs recommend the suffix for new code[1][2]. Prefixes (like tw:) are separate and always at the beginning as variants, e.g., tw:bg-red-500! for both prefix and important[4][2].
Citations:
- 1: https://tailwindcss.com/docs/upgrade-guide
- 2: https://tailwindcss.com/docs/styling-with-utility-classes
- 3: [v4] Support old important modifier position tailwindlabs/tailwindcss#13103
- 4: Add support for prefixes tailwindlabs/tailwindcss#14501
🏁 Script executed:
# Check for Tailwind configuration files
fd -i 'tailwind.config' -o 'tailwind.{js,ts,cjs,mjs}'
# Also check tsconfig or other config files that might affect this
fd 'tailwind.config\.' -type fRepository: Jingli-123/bookified
Length of output: 403
🏁 Script executed:
# Find tailwind config files correctly
find . -maxdepth 2 -name "tailwind.config.*" -type f
# Also check for any CSS files that might have Tailwind config
fd 'tailwind' -type fRepository: Jingli-123/bookified
Length of output: 235
🏁 Script executed:
# Find tailwind config files
find . -name "tailwind.config*" 2>/dev/null
# Also check the root for any config
ls -la | grep -i tailwindRepository: Jingli-123/bookified
Length of output: 46
Update Tailwind important modifiers to v4 suffix syntax.
These classes use v3-style prefix syntax (e.g., !text-2xl). While this is still supported in Tailwind v4 for backwards compatibility, it is deprecated. Migrate to the v4 suffix syntax (e.g., text-2xl!):
Examples from lib/constants.ts (lines 157–167)
- '!text-2xl font-bold text-[`#212a3b`]'
+ 'text-2xl! font-bold text-[`#212a3b`]'
- '!mt-3 !text-sm text-[`#3d485e`]'
+ 'mt-3! text-sm! text-[`#3d485e`]'
- '!border border-[rgba(33,42,59,0.12)] hover:bg-[`#212a3b`]/10 transition-all h-12 text-lg !rounded-xl'
+ 'border! border-[rgba(33,42,59,0.12)] hover:bg-[`#212a3b`]/10 transition-all h-12 text-lg rounded-xl!'
Also affects app/globals.css where CSS !important declarations should be replaced with the Tailwind suffix syntax when applicable.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/constants.ts` around lines 157 - 167, Update Tailwind important modifiers
from v3 prefix syntax to v4 suffix syntax for the listed constants: replace
leading '!' in values for headerTitle, headerSubtitle, socialButtonsBlockButton,
socialButtonsBlockButtonText, formButtonPrimary, formFieldInput, formFieldLabel,
and footerActionLink with trailing '!' on the corresponding utility classes
(e.g., change '!text-2xl' → 'text-2xl!'; '!mt-3 !text-sm' → 'mt-3! text-sm!';
'!border' → 'border!' etc.). Ensure combined utilities preserve spacing and
other tokens (colors, hover, shadow) and do the same migration for any
`!important` usages in app/globals.css by converting applicable Tailwind
utilities to the suffix form or removing raw !important in favor of the suffix
syntax.
| export const getCurrentBillingPeriodStart = (): Date => { | ||
| const now = new Date(); | ||
| return new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); | ||
| }; |
There was a problem hiding this comment.
Use UTC for billing-period boundaries.
Line 39 computes the month start in server-local time, which can shift billing cutoffs across regions/DST. Use UTC to keep period windows deterministic.
Suggested fix
export const getCurrentBillingPeriodStart = (): Date => {
const now = new Date();
- return new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
+ return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0));
};📝 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.
| export const getCurrentBillingPeriodStart = (): Date => { | |
| const now = new Date(); | |
| return new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0); | |
| }; | |
| export const getCurrentBillingPeriodStart = (): Date => { | |
| const now = new Date(); | |
| return new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1, 0, 0, 0, 0)); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@lib/subscription-constants.ts` around lines 37 - 40,
getCurrentBillingPeriodStart currently builds the month-start in server-local
time; change it to compute the start using UTC so billing boundaries are
deterministic across regions/DST—use now.getUTCFullYear() and now.getUTCMonth()
(or Date.UTC(year, month, 1)) and return new Date(...) created from that UTC
timestamp in the getCurrentBillingPeriodStart function.
Summary by CodeRabbit
Release Notes
New Features
Documentation
Chores