Skip to content

Full review#2

Merged
Jingli-123 merged 8 commits into
mainfrom
full-review
May 6, 2026
Merged

Full review#2
Jingli-123 merged 8 commits into
mainfrom
full-review

Conversation

@Jingli-123

@Jingli-123 Jingli-123 commented May 6, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

Release Notes

  • New Features

    • Browse and manage a personal book library with sample catalog
    • Upload PDF books with custom titles, authors, and voice personas
    • Engage with books through voice-based conversations with AI synthesis
    • Search across your entire book collection
    • Choose from multiple voice personas for personalized audio interactions
    • Tiered subscription plans with usage limits and session controls
  • Documentation

    • Added project setup and deployment guides
  • Chores

    • Added project configuration and build infrastructure

@vercel

vercel Bot commented May 6, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
bookified Error Error May 6, 2026 4:36am

@coderabbitai

coderabbitai Bot commented May 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: d765c8ac-8ffc-441d-833e-342145a7cea0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch full-review

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 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 win

Expose 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 win

Use the custom Toaster wrapper from components/ui/sonner instead of importing directly from "sonner".

app/layout.tsx (line 15) imports Toaster from "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 win

Context guard is currently ineffective.

Because both contexts default to {} casts, Line 52's if (!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 } = itemContext

Also 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 win

Use 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 win

Duplicate import of getCurrentBillingPeriodStart.

getCurrentBillingPeriodStart is 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

formatDuration may produce unexpected output with fractional seconds.

If seconds contains a decimal (e.g., 91.5), the modulo operation will preserve the fraction (31.5 → displays as 1: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 win

Unnecessary revalidatePath on 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 win

Fix 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 win

Remove 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 win

Mark 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 win

Use 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 value

Redundant individual index on clerkId.

The compound index { clerkId: 1, billingPeriodStart: 1 } on line 13 already covers queries that filter only by clerkId. The individual index on clerkId (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 win

Missing validation for durationSeconds parameter.

durationSeconds is 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 value

Unnecessary 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 value

Consider adding an index on clerkId.

If queries frequently filter books by user (e.g., "get all books for a user"), an index on clerkId would improve performance. The slug field already has implicit indexing via unique: 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 value

Type safety consideration for value assertion.

The value as File cast on line 71 assumes value is always a File when isUploaded is 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2ec7012 and e18ac8b.

⛔ Files ignored due to path filters (16)
  • app/favicon.ico is excluded by !**/*.ico
  • package-lock.json is excluded by !**/package-lock.json
  • public/assets/Gemini_Generated_Image_jlix6fjlix6fjlix (1) 1.png is excluded by !**/*.png
  • public/assets/banner.svg is excluded by !**/*.svg
  • public/assets/book-cover.svg is excluded by !**/*.svg
  • public/assets/book.png is excluded by !**/*.png
  • public/assets/desktop-banner.png is excluded by !**/*.png
  • public/assets/hero-illustration.png is excluded by !**/*.png
  • public/assets/image.png is excluded by !**/*.png
  • public/assets/loader.png is excluded by !**/*.png
  • public/assets/logo-full.svg is excluded by !**/*.svg
  • public/assets/logo.png is excluded by !**/*.png
  • public/assets/mobile-banner.png is excluded by !**/*.png
  • public/assets/open-book.svg is excluded by !**/*.svg
  • public/assets/tab-banner.png is excluded by !**/*.png
  • public/assets/wave.png is excluded by !**/*.png
📒 Files selected for processing (42)
  • .gitignore
  • AGENTS.md
  • CLAUDE.md
  • README.md
  • app/(root)/books/new/page.tsx
  • app/(root)/page.tsx
  • app/globals.css
  • app/layout.tsx
  • components.json
  • components/BookCard.tsx
  • components/FileUploader.tsx
  • components/HeroSection.tsx
  • components/LoadingOverlay.tsx
  • components/Navbar.tsx
  • components/Search.tsx
  • components/Transcript.tsx
  • components/VapiControls.tsx
  • components/VoiceSelector.tsx
  • components/ui/button.tsx
  • components/ui/form.tsx
  • components/ui/input.tsx
  • components/ui/label.tsx
  • components/ui/radio-group.tsx
  • components/ui/sonner.tsx
  • components/uploadForm.tsx
  • database/models/book-segment.model.ts
  • database/models/book.model.ts
  • database/models/voice-session.model.ts
  • database/mongoose.ts
  • eslint.config.mjs
  • lib/actions/book.actions.ts
  • lib/actions/session.actions.ts
  • lib/constants.ts
  • lib/subscription-constants.ts
  • lib/subscription.server.ts
  • lib/utils.ts
  • lib/zod.ts
  • next.config.ts
  • package.json
  • postcss.config.mjs
  • tsconfig.json
  • types.d.ts

Comment thread app/globals.css
Comment on lines +931 to +953
/* 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";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🏁 Script executed:

# First, let's check the full CSS file around the lines mentioned
wc -l app/globals.css

Repository: Jingli-123/bookified

Length of output: 84


🏁 Script executed:

# Look at the CSS context more broadly
sed -n '920,960p' app/globals.css

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

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

Repository: Jingli-123/bookified

Length of output: 92


🏁 Script executed:

# Check if there's any Clerk configuration or integration code
fd -t f "clerk" --iglob

Repository: Jingli-123/bookified

Length of output: 267


🏁 Script executed:

# Search for JavaScript/TypeScript files
fd -e js -e ts | head -20

Repository: Jingli-123/bookified

Length of output: 375


🏁 Script executed:

# Look for files containing Clerk or pricing references
rg "Clerk|pricing" --type-list | head

Repository: Jingli-123/bookified

Length of output: 454


🏁 Script executed:

# Search for any files containing "pricingTableCard"
rg "pricingTableCard" -l

Repository: Jingli-123/bookified

Length of output: 80


🏁 Script executed:

# Search for Clerk imports or pricing component usage
rg "from.*clerk|import.*clerk|@clerk" -i

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

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

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

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

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

Repository: Jingli-123/bookified

Length of output: 1402


🏁 Script executed:

# Look at the commented CSS to understand original structure
sed -n '960,1000p' app/globals.css

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

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

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

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

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

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

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

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

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

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

Repository: Jingli-123/bookified

Length of output: 129


🏁 Script executed:

# Investigate the Navbar.tsx to see if it contains pricing
cat components/Navbar.tsx | head -50

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

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


🏁 Script executed:

# Look for the subscriptions page mentioned in Navbar
find . -path "*/subscriptions*" -type f

Repository: Jingli-123/bookified

Length of output: 46


🏁 Script executed:

# Check app structure more carefully
find app -type d | head -20

Repository: Jingli-123/bookified

Length of output: 117


🏁 Script executed:

# Look for the subscriptions route
find . -name "*subscriptions*" -o -name "*pricing*" 2>/dev/null

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

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

Comment thread components/Search.tsx
Comment on lines +13 to +29
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]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 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 -20

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


🏁 Script executed:

# Check if the file exists and examine its structure to understand the full context
find . -name "Search.tsx" -type f 2>/dev/null

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

Repository: Jingli-123/bookified

Length of output: 145


🏁 Script executed:

cat -n components/Search.tsx

Repository: Jingli-123/bookified

Length of output: 1942


🏁 Script executed:

# Check if searchParams is imported from next/navigation
head -20 components/Search.tsx

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

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

Comment thread components/ui/button.tsx
Comment on lines +41 to +60
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}
/>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

git ls-files | grep -E "(button|Button)" | head -20

Repository: Jingli-123/bookified

Length of output: 89


🏁 Script executed:

cat -n components/ui/button.tsx

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

Comment thread components/uploadForm.tsx
Comment on lines +73 to +99
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

🧩 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.tsx

Repository: Jingli-123/bookified

Length of output: 94


🏁 Script executed:

# Read the entire file to understand the full upload flow
cat -n components/uploadForm.tsx

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

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

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

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

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

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

Repository: Jingli-123/bookified

Length of output: 96


🏁 Script executed:

# Read the full book.actions.ts file
cat -n lib/actions/book.actions.ts

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

Comment on lines +66 to +76
<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>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Comment on lines +149 to +159
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);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n lib/actions/book.actions.ts | head -200

Repository: Jingli-123/bookified

Length of output: 7231


🏁 Script executed:

rg "saveBookSegments" --type ts --type tsx -B 2 -A 2

Repository: Jingli-123/bookified

Length of output: 92


🏁 Script executed:

rg "saveBookSegments" -B 2 -A 2

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

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

Comment on lines +205 to +211
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' },

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

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

Comment thread lib/constants.ts
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!;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

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

Comment thread lib/constants.ts
Comment on lines +157 to +167
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',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ 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:


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

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

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

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

Comment on lines +37 to +40
export const getCurrentBillingPeriodStart = (): Date => {
const now = new Date();
return new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

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

@Jingli-123 Jingli-123 merged commit f7c8165 into main May 6, 2026
2 of 3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant