AUDIT.md

Thermo-Nuclear Code Audit — Findings

Multi-agent audit of the codebase (quality, maintainability, performance, modernization), following the thermo-nuclear-code-quality-review skill. Each finder's output was adversarially verified before landing here.

Format: problem type - actual problem - file/files it touches

Findings

Critical (6)

  • duplication - The SCDW insurance pricing formula (calculateSCDW: base 2x daily rate, +6 then +5 per 3-day block) is copy-pasted in 4 files, and the copies are fed divergent daily rates: the reservation page charges seasonal price, additional-features-card displays base price with no seasonal multiplier, and edit-reservation-dialog uses seasonal in one path but base in another within the same file — so the SCDW price shown to the customer can differ from what is charged and from what admins recompute. Extract one canonical implementation into lib/vehicle-utils.ts (or a dedicated pricing module) with a defined rate input. - app/[locale]/reservation/page.tsx, components/admin/reservations/edit-reservation-dialog.tsx, components/features/reservations/additional-features-card.tsx, hooks/use-reservation-pricing.ts, lib/vehicle-utils.ts
  • file-size - The reservation page is an 1888-line 'use client' God-component with 19 useState hooks owning localStorage persistence, the full pricing engine (computed twice: a display path and a divergent submit path), Zod validation, submission orchestration, and all card JSX; every keystroke re-renders the entire tree and re-executes the unmemoized calculateTotalPrice() in the render body. A decomposition (hooks/use-reservation-form.ts, use-reservation-pricing.ts, three card components) was started and abandoned — none of it is imported. Split into memoized child components with a single memoized pricing hook shared by summary UI and submit payload. - app/[locale]/reservation/page.tsx, hooks/use-reservation-form.ts, hooks/use-reservation-pricing.ts, components/features/reservations/personal-info-card.tsx, components/features/reservations/payment-method-card.tsx, components/features/reservations/additional-features-card.tsx
  • performance - createReservation (reservations.ts:80-86) and createTransfer (transfers.ts:64-69) compute the next sequential booking number by .collect()-ing the ENTIRE table and reducing for the max on every single booking. Cost grows linearly with lifetime bookings, will hit Convex transaction read limits, and reading the whole table into the mutation's read set makes concurrent bookings OCC-conflict and retry, serializing the money path. Fix: a counter document, or an index on reservationNumber/transferNumber read via .withIndex().order('desc').first(). - convex/reservations.ts, convex/transfers.ts, convex/schema.ts
  • performance - app/layout.tsx (root layout) calls await headers() to read x-pathname solely to compute ; a dynamic API in the root layout opts every route into per-request rendering, defeating generateStaticParams for home, /cars, /blog, and all car/blog detail pages, which hit Convex via fetchQuery on every request instead of being statically prerendered/ISR'd. Derive lang from the [locale] segment instead of headers(). - app/layout.tsx, proxy.ts, app/[locale]/layout.tsx, app/[locale]/cars/[slug]/page.tsx, app/[locale]/blog/[slug]/page.tsx
  • security - Admin-only Convex write functions are public internet endpoints with zero auth checks: all vehicles, blogs, seasons, transferPricing, and vehicleClasses create/update/remove/reorder/upload mutations. Admin enforcement exists only in the Next.js Clerk middleware (proxy.ts), which direct calls to the Convex deployment bypass — anyone can call api.vehicles.remove or api.seasons.setCurrent. Add a shared requireAdmin(ctx) helper or an adminMutation wrapper to every admin write. - convex/vehicles.ts, convex/blogs.ts, convex/seasons.ts, convex/transferPricing.ts, convex/vehicleClasses.ts, proxy.ts
  • wrong-layer - createReservation and createTransfer are public, unauthenticated mutations that trust client-supplied money fields (totalPrice, protectionCost, deductibleAmount, baseFare, distancePrice, seasonalMultiplier) and store them verbatim, even sending confirmation emails at the client-chosen price — any client can submit arbitrary totals. All pricing rules (warranty table, SCDW, days * 3 extras, tier/seasonal math, location fees) live only in client code; server-side building blocks are query-only or missing. Move pricing into a pure shared module and recompute/validate totals inside the create mutations. - convex/reservations.ts, convex/transfers.ts, convex/transferPricing.ts, convex/seasons.ts, app/[locale]/reservation/page.tsx, lib/vehicle-utils.ts, components/features/reservations/additional-features-card.tsx

High (30)

  • bug - VehicleOrderingPage syncs Convex data with if (vehicles && items.length === 0) setItems([...vehicles]) executed during render; for a class with zero vehicles this sets a new empty array every render, the condition never turns false, and React throws 'Too many re-renders' — the page crashes in exactly the empty-state case. It also never resyncs once items is non-empty. Use a useEffect keyed on [vehicles] or derive items from the query. - app/admin/vehicles/classes/[classId]/page.tsx
  • bug - Image uploads ship raw file bytes through Convex action arguments: clients read every file into an ArrayBuffer and pass them all in one uploadImages call (v.array(v.bytes()) in vehicles, v.array(v.any()) plus an unused blogId arg in blogs, and a useAction(... as any) cast in use-image-upload.tsx). Convex caps function args at ~16MiB total / ~1MiB per byte value while the UI allows 10x10MB files, so realistic uploads fail outright. Also vehicles.uploadImages persists via the public unauthenticated api.vehicles.update instead of an internal mutation. Fix: ctx.storage.generateUploadUrl() + direct POST per file, persisting storage IDs via a narrow mutation. - hooks/use-image-upload.tsx, components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/blog/create-blog-dialog.tsx, components/admin/blog/edit-blog-dialog.tsx, convex/vehicles.ts, convex/blogs.ts
  • bug - vehicles.searchAvailableVehicles declares startDate/endDate/deliveryLocation args but never uses them and never consults the reservations table, so it returns vehicles regardless of reservation overlap for the requested dates. It currently has no in-repo callers (leftover migration code), but it is an exported public Convex query with a misleading contract — either implement the overlap check (by_dates index exists) or delete it. - convex/vehicles.ts
  • convex-convention - Blog queries bypass the existing by_status index: getAll, getFeatured, getPublished, and getAlternateSlugs (used by the sitemap) full-scan the blogs table with .filter(), reading full bilingual article bodies including drafts on every public blog page render (and live again in BlogListClient); setFeatured also scans on unindexed isFeatured, and generateStaticParams runs getAll twice at build. Fix with .withIndex("by_status", q => q.eq("status","published")) (ordering is preserved), an isFeatured-capable index, and a lightweight slugs-only query for static params. - convex/blogs.ts, convex/schema.ts, app/[locale]/blog/page.tsx, app/[locale]/blog/[slug]/page.tsx
  • convex-convention - The vehicles table has no index on classId, so vehicles.getByClass (used by the admin class-ordering page) and the in-use check in vehicleClasses.remove do .filter() full-table scans of vehicles. Add .index('by_class', ['classId']) to schema.ts (optionally composite with classSortIndex to drop the JS sort) and switch both call sites to withIndex. - convex/schema.ts, convex/vehicles.ts, convex/vehicleClasses.ts, app/admin/vehicles/classes/[classId]/page.tsx
  • dead-code - The abandoned reservation-page decomposition ships as dead code: PersonalInfoCard, PaymentMethodCard, AdditionalFeaturesCard, the index.ts barrel, useReservationForm (only its interfaces are imported), useReservationPricing, and lib/reservation-utils.ts are imported by nothing live. The dead pricing hook has drifted dangerously from the real implementation (raw differenceInDays instead of calculateRentalDays, no seasonal multiplier, vehicle?: any), and lib/reservation-utils.ts carries hardcoded English strings. Either make this cluster canonical (shrinking the 1888-line page) or delete it. - components/features/reservations/personal-info-card.tsx, components/features/reservations/payment-method-card.tsx, components/features/reservations/additional-features-card.tsx, components/features/reservations/index.ts, hooks/use-reservation-form.ts, hooks/use-reservation-pricing.ts, lib/reservation-utils.ts, app/[locale]/reservation/page.tsx
  • dead-code - The '-with-preloaded-images' fork left the originals orphaned: VehicleCard, VehicleListDisplay, VehicleImage, and three skeletons are reachable only via the components/features/vehicles/index.ts barrel, which no file imports. The pairs are near-verbatim copies (~200 duplicated lines of grouping/sorting logic) that have already drifted in styling, and vehicle-card.tsx still contains a per-card getImageUrl N+1. Delete the dead originals and barrel, or unify each pair into one component taking an imageUrl prop. - components/features/vehicles/index.ts, components/features/vehicles/vehicle-card.tsx, components/features/vehicles/vehicle-card-with-preloaded-image.tsx, components/features/vehicles/vehicle-list-display.tsx, components/features/vehicles/vehicle-list-display-with-preloaded-images.tsx, components/features/vehicles/vehicle-image.tsx, components/features/vehicles/vehicle-card-skeleton.tsx, components/features/vehicles/vehicle-filters-skeleton.tsx, components/features/vehicles/vehicle-search-form-skeleton.tsx
  • dead-code - The entire lib email stack — lib/email-factory.ts (a class of 5 static identity wrappers taking any params, with 'To be created' template entries), lib/email-utils.ts, and types/email.ts — is only referenced by itself; the live implementation is convex/emails/{types,utils}.ts, whose header admits 'Duplicated from types/email.ts' (based on the mistaken belief that Convex cannot import code from outside convex/). Delete the lib copies and make convex/emails (or one shared plain-TS module) the single source. - lib/email-factory.ts, lib/email-utils.ts, types/email.ts, convex/emails/types.ts, convex/emails/utils.ts
  • duplication - The confirmation page reimplements rental-day math (computeRentalDays) instead of reusing lib/vehicle-utils.calculateRentalDays, and the copy has diverged (an unreachable branch, different rounding). Root cause: pricePerDayUsed is sent to createReservation but never persisted, so the confirmation must recompute price-per-day from current vehicle tiers/season and its breakdown can silently disagree with the stored totalPrice if pricing changes after booking. Persist days/pricePerDay on the reservation and render stored values. - app/[locale]/reservation/confirmation/page.tsx, lib/vehicle-utils.ts, convex/reservations.ts, convex/schema.ts
  • duplication - Company identity data (phone +40 773 932 961, office@rngo.ro, street address, geo coordinates, social URLs) is hardcoded in 10+ places: JSON-LD schemas in five layouts/pages, inline text in contact/terms/privacy/both confirmation pages, translation JSONs, convex/emails.ts, and the reservation-email API route. Copies have already drifted (transfer confirmation shows contact@rngo.ro vs office@rngo.ro everywhere else). Extract a lib/company.ts constants module plus shared JSON-LD schema builders. - app/[locale]/page.tsx, app/[locale]/about/layout.tsx, app/[locale]/transfers/layout.tsx, app/[locale]/contact/layout.tsx, app/[locale]/cars/[slug]/page.tsx, app/[locale]/contact/page.tsx, app/[locale]/terms/page.tsx, app/[locale]/privacy/page.tsx, app/[locale]/reservation/confirmation/page.tsx, app/[locale]/transfers/confirmation/[transferId]/page.tsx, app/api/send/reservation-email/route.ts, messages/en.json, messages/ro.json, convex/emails.ts
  • duplication - transfers/booking/page.tsx re-implements the reservation checkout: a third copy of the payment-methods list (already drifted in descriptions/disabled flags, and untranslated English), a verbatim copy of the terms-acceptance locale ternary, and the same name/email/phone input-plus-error pattern; the existing shared PaymentMethodCard is used by neither page. Consolidate on shared, parametrized checkout components with translated labels (method IDs plus translation keys, not the hardcoded-English lib/reservation-utils.ts list). - app/[locale]/transfers/booking/page.tsx, app/[locale]/reservation/page.tsx, lib/reservation-utils.ts, components/features/reservations/payment-method-card.tsx
  • duplication - All three create/edit admin dialog pairs are ~90% copy-paste: vehicles (1088/1145 lines, ~233 diff lines, both over the 1000-line threshold), blogs (835/869, ~74 differ, duplicating LangToggle/ImageThumbnail subcomponents and blogSchema), seasons (338/373, ~77 differ). Each pair duplicates the zod schema, upload flow, editors, and full form JSX. Extract shared // plus one schema per entity, leaving create/edit as thin wrappers. - components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/vehicles/edit-vehicle-dialog.tsx, components/admin/blog/create-blog-dialog.tsx, components/admin/blog/edit-blog-dialog.tsx, components/admin/seasons/create-season-dialog.tsx, components/admin/seasons/edit-season-dialog.tsx
  • duplication - The transfer pricing formula (BASE_KM_INCLUDED/DEFAULT_BASE_FARE constants, tier lookup, extraKm * tierPrice * classMultiplier, round-trip doubling) exists in three near-identical copies: calculateTransferPrice and calculateTransferPriceByVehicle in transferPricing.ts (~95% identical), plus a third inline copy in transfers.getTransferVehiclesWithImages with re-declared constants. Extract one pure computeTransferPrice helper — a formula change currently requires three synchronized edits. - convex/transferPricing.ts, convex/transfers.ts
  • duplication - convex/transfers.ts hand-rolls the identity -> by_clerk_id lookup -> role==='admin' check six times plus a seventh user lookup, instead of reusing getCurrentUser/getCurrentUserOrThrow from users.ts as reservations.ts and featuredCars.ts do; failure modes are inconsistent (queries silently return []/zeroed stats, mutations throw), and cancelTransfer skips the check entirely — exactly the omission a shared requireAdmin(ctx) helper would have prevented. - convex/transfers.ts, convex/users.ts, convex/reservations.ts, convex/featuredCars.ts
  • duplication - lib/season-utils.ts is an admitted line-for-line mirror of convex/seasons.ts getMultiplierForDateRange (dateToNumber, addDays, getDatesInRange, isDateInPeriod, and the ~143-line overlap-winner algorithm with inline helpers), and the Convex query has zero callers — every pricing surface uses the client copy, leaving the server version a dead parallel implementation of pricing-critical logic that will silently drift; the recurring MM-DD semantics also contradict the neighboring isDateInSeason internalQuery. Extract the pure date/multiplier functions into one shared module (typed off Doc<"seasons">) imported by both sides, or delete the dead server copy. - lib/season-utils.ts, convex/seasons.ts, hooks/use-date-based-seasonal-pricing.ts
  • file-size - edit-reservation-dialog.tsx is 1265 lines and repeats the 'vehicleId + dates + times -> find vehicle -> calculateVehiclePricing' block five times; three copies also repeat the seasonal-multiplier tail, and they have diverged — the SCDW useEffect prices from getBasePricePerDay while calculateSuggestedPrice uses the seasonal price, so auto-filled protectionCost and the computed total can disagree under seasonal multipliers. Extract a usePriceDetails() helper, a PricingBreakdown subcomponent, and the zod schema module. - components/admin/reservations/edit-reservation-dialog.tsx
  • i18n - Large parts of the localized site show hardcoded English on a Romanian-default product: the entire transfers flow (features grid, payment labels, toasts, 'Missing Transfer Details' screens, 'Pickup/Dropoff/Date/Time/Passengers/Return Trip/Transfer Not Found'), transfer components (transfer-search-form, transfer-vehicle-list, transfer-summary-card, mapbox-location-search including its internal strings and language:'en'), blog-list-client pluralization, the SCDW explainer in additional-features-card, toasts in booking and contact pages, and app/[locale]/error.tsx. Move all of these into messages/en.json + ro.json like adjacent code. - app/[locale]/transfers/page.tsx, app/[locale]/transfers/booking/page.tsx, app/[locale]/transfers/vehicles/page.tsx, app/[locale]/transfers/confirmation/[transferId]/page.tsx, components/features/transfers/transfer-search-form.tsx, components/features/transfers/transfer-vehicle-list.tsx, components/features/transfers/transfer-summary-card.tsx, components/shared/search-filters/mapbox-location-search.tsx, components/features/blog/blog-list-client.tsx, components/features/reservations/additional-features-card.tsx, app/[locale]/contact/page.tsx, app/[locale]/error.tsx
  • performance - mapbox-gl (~1.5 MB parsed, ~230 KB gzip) plus its CSS is statically imported by transfer-route-map.tsx, which is statically imported into the transfers vehicles page, the booking page (via TransferSummaryCard), and the confirmation page — the whole GL runtime lands in those routes' initial JS bundles. Since mapbox is only used inside useEffect, load TransferRouteMap via next/dynamic({ ssr: false }) to code-split it. - components/features/transfers/transfer-route-map.tsx, components/features/transfers/transfer-summary-card.tsx, app/[locale]/transfers/booking/page.tsx, app/[locale]/transfers/vehicles/page.tsx, app/[locale]/transfers/confirmation/[transferId]/page.tsx
  • performance - The homepage hero/LCP background is public/mercedes-background.png, a 1.9 MB 2048x1054 RGBA PNG loaded as a Tailwind CSS background-image in BackgroundImage (also used on the about page), bypassing next/image entirely: no resizing, no AVIF/WebP, late discovery after CSS parse, no preload/fetchpriority — likely dominating LCP. Replace with next/image fill+priority on a compressed asset; public/our-story.jpg (808 KB) is also oversized at source. - components/ui/background-image.tsx, app/[locale]/home-page-client.tsx, app/[locale]/about/page.tsx, public/mercedes-background.png, public/our-story.jpg
  • performance - Vehicle card images set next/image priority unconditionally (vehicle-card-with-preloaded-image.tsx:120, vehicle-card.tsx:125); on /cars the entire fleet is rendered, so every image gets a preload hint and eager loading, defeating lazy loading and saturating bandwidth meant for above-the-fold content. Accept an index/priority prop and set priority only for the first ~3-6 cards. - components/features/vehicles/vehicle-card-with-preloaded-image.tsx, components/features/vehicles/vehicle-card.tsx, components/features/vehicles/vehicle-list-display-with-preloaded-images.tsx, app/[locale]/cars/cars-page-client.tsx, app/[locale]/cars/page.tsx
  • performance - Three table surfaces (admin reservation-table, admin transfers page, user-reservations-table) each render a per-row VehicleInfo component running its own useQuery(api.vehicles.getById) — an N+1 of up to 10 Convex subscriptions per page with 'Loading...' flicker — and in all three the component is declared inside the parent's function body, so its type identity changes every render and React remounts every cell and re-establishes subscriptions on any re-render (e.g. pagination). Join vehicle make/model/year server-side in getAllReservations/getAllTransfers/getCurrentUserReservations and delete the inline components. - components/admin/reservations/reservation-table.tsx, app/admin/transfers/page.tsx, components/features/reservations/user-reservations-table.tsx, convex/reservations.ts, convex/transfers.ts
  • performance - Admin list queries return entire growing tables with no pagination: reservations.getAllReservations and transfers.getAllTransfers do unbounded .collect() (transfers also JS-sorts instead of using the by_pickup_date index), shipping every document including customer PII and message fields to the browser, where the tables paginate client-side with .slice(). These will hit Convex's 8MiB/16k-doc query limits and re-push the full list on every write; convert to .paginate() with usePaginatedQuery (the dashboard's stats usage of getAllReservations needs a separate count query). - convex/reservations.ts, convex/transfers.ts, components/admin/reservations/reservation-table.tsx, app/admin/transfers/page.tsx, app/admin/page.tsx
  • performance - All four admin stats queries run unbounded full-table scans on reactive queries: getReservationStats, getMonthlyChartData, getTransferStats, and getMonthlyTransferChartData each .collect() the entire table and filter by month/status in JS, so every reservation/transfer write re-executes multiple full scans while a dashboard is open; the reservation and transfer stat/chart implementations are also copy-pasted from each other. Use index range scans (_creationTime bounds, by_pickup_date, by_status) or maintained aggregates; the all-time totals need a counter since they cannot be date-bounded. - convex/reservations.ts, convex/transfers.ts, app/admin/reservations/page.tsx, app/admin/transfers/page.tsx, components/admin/reservations/reservation-table.tsx
  • performance - In edit-reservation-dialog.tsx a top-level form.watch([...8 fields]) plus ~15 inline form.watch calls subscribe the whole 1265-line dialog to every pricing-field change, and each of the N vehicle SelectItems calls getRentalDays() (5 form.watch calls + O(N) vehicles.find + full calculateVehiclePricing) whose result is identical for all items — N+3 redundant pricing runs per render. create-reservation-dialog shares the milder top-level form.watch pattern. Compute days/seasonal price once per render in a useMemo and move watch-dependent UI into useWatch subcomponents. - components/admin/reservations/edit-reservation-dialog.tsx, components/admin/reservations/create-reservation-dialog.tsx
  • security - cancelTransfer and cancelReservation authenticate the caller but never compare the record's userId to the caller, so any signed-in user can cancel anyone's booking by ID. Additionally getTransferById, getTransfersByVehicle, and getReservationsByVehicle are fully public queries returning customerInfo PII (name, email, phone, flight number); vehicle IDs are publicly visible, making customer PII trivially enumerable without authentication. - convex/transfers.ts, convex/reservations.ts
  • security - app/api/send/reservation-email/route.ts performs no authentication — any anonymous visitor can POST and send arbitrary HTML email from noreply@rngo.ro via the Resend account (it is only called from an admin dialog, so it should verify the Clerk session/admin role). It also inlines a ~140-line HTML email template despite the canonical template system under convex/emails/templates. - app/api/send/reservation-email/route.ts, components/admin/reservations/reservation-email-dialog.tsx, proxy.ts
  • type-safety - blogs.getBySlug and getAlternateSlug dispatch on locale via withIndex(indexName as any, (q: any) => q.eq(slugField, slug)), defeating index typing entirely, while incrementViews does the same lookup a third time with a typed if/else; the locale-index-then-legacy-fallback lookup is triplicated. Extract a typed findBlogBySlug(ctx, slug, locale) helper with an explicit if/else on locale, deleting both as any casts and the triplication. - convex/blogs.ts
  • wrong-layer - The admin overview dashboard subscribes to ALL reservations and vehicles client-side (getAllReservations/getAllVehicles) and re-implements monthly revenue, weekly counts, and 6-month revenue aggregation inline in the component with no useMemo — while convex/reservations.ts already exposes getReservationStats and getMonthlyChartData used by the reservations page. The existing aggregates need small extensions (vehicle count, last-7-days series, startDate bucketing) but the aggregation belongs server-side, not shipped-whole-table client math that will diverge from the canonical stats. - app/admin/page.tsx, convex/reservations.ts
  • wrong-layer - The shared DateTimePicker imports the car-rental searchStorage and silently persists dates/times based on magic string sniffing (id.includes("pickup")/id.includes("return")); the transfer form's picker ids match those substrings, so selecting transfer dates overwrites the user's saved rental search in localStorage — cross-feature state bleed from a shared component that also double-persists what parent forms already save. Make DateTimePicker fully controlled and move persistence into the owning forms. - components/shared/search-filters/date-time-picker.tsx, components/features/transfers/transfer-search-form.tsx, lib/search-storage.ts
  • wrong-layer - Business pricing data — the LOCATION_DATA table of pickup/return locations with delivery fees in EUR — is hardcoded inside the shared UI component location-picker.tsx, and lib/vehicle-utils.ts plus hooks import getLocationPrice from a component (inverted lib -> component dependency); revenue-bearing fees are invisible to the server. Move LOCATION_DATA/getLocationPrice into lib (or the Convex backend alongside seasons) and have the picker consume it. - components/shared/search-filters/location-picker.tsx, lib/vehicle-utils.ts, hooks/use-reservation-pricing.ts

Medium (53)

  • bug - In edit-reservation-dialog the auto-recalc useEffect passes the form.watch([...]) result array directly as the dependency array and unconditionally setValue("totalPrice", ...), so the admin's manual price edits are silently clobbered (and the deps include a freshly-created additionalCharges array reference); a second effect builds its deps from inline form.watch() calls. Replace with an RHF watch subscription or apply the suggested price only via the explicit Recalc button. - components/admin/reservations/edit-reservation-dialog.tsx
  • bug - modern-image-upload-preview.tsx leaks blob object URLs: the unmount cleanup effect has an empty dependency array so its closure captures the initial files prop, and any preview URLs created afterwards are never revoked (~10MB per image alive for the tab lifetime); parent dialogs also reset state on close/submit without revoking. Keep the latest files in a ref and revoke from it in cleanup. - components/ui/modern-image-upload-preview.tsx, components/admin/vehicles/create-vehicle-dialog.tsx
  • bug - mapbox-location-search.tsx embeds raw Mapbox Search Box suggest/retrieve calls, session-token lifecycle, and debounce in the component (bypassing lib/mapbox.ts and getMapboxToken()), and on retrieve failure silently fabricates {lng: 0, lat: 0} while reporting the selection as successful — those coordinates feed getRouteInfo and the distance-based transfer price, producing a silently missing route or a bogus 0-km base-fare price with no visible error. - components/shared/search-filters/mapbox-location-search.tsx, lib/mapbox.ts, components/features/transfers/transfer-search-form.tsx
  • bug - Internal navigation uses window.location.href with non-localized paths: about/page.tsx navigates to "/contact" (full reload plus a middleware locale-redirect hop, losing the /[locale] prefix) and profile/page.tsx navigates to "/login", a route that does not exist — sign-in is handled by Clerk's SignInButton modal — so the profile sign-in button lands on a 404. Use locale-aware Link navigation and a Clerk SignInButton. - app/[locale]/about/page.tsx, app/[locale]/profile/page.tsx
  • convex-convention - Widespread .filter() where indexes exist, violating the repo's own Convex guidelines: vehicles.getAll and searchAvailableVehicles filter on type/status/transmission despite by_type/by_status indexes (getAll's paginated variant scans past non-matching rows per page), and featuredCars.setFeaturedCar filters on vehicleId with no index (bounded to 3 rows, convention-only). Use .withIndex for the selective predicate and keep the rest as post-filters, minding that switching getAll to an index changes its _creationTime-desc ordering. - convex/vehicles.ts, convex/featuredCars.ts, convex/schema.ts
  • convex-convention - Many public Convex functions omit the returns validators required by .cursor/rules/convex_rules.mdc and CLAUDE.md: most of vehicles.ts, nearly all of transfers.ts and reservations.ts, all 6 featuredCars.ts functions, and blogs.getPublished; several mutations return ad-hoc unvalidated { success: true, ... } shapes, skipping runtime output validation and leaking untyped contracts to clients. - convex/vehicles.ts, convex/transfers.ts, convex/reservations.ts, convex/featuredCars.ts, convex/blogs.ts
  • dead-code - VehicleSearchFilterForm carries a dead dual controlled/uncontrolled mode: 6 internal useState hooks, 6 setter shims, two localStorage effects, and 4 initial* props — but its only call site (home-page-client.tsx) always passes searchState + updateSearchField, so the entire internal-state fallback (~130 lines, half the file) is unreachable. Make the controlled props required and delete the fallback. - components/features/vehicles/vehicle-search-filter-form.tsx, app/[locale]/home-page-client.tsx
  • dead-code - Dead hooks and libs nothing imports: hooks/use-vehicle-list.ts (imperative convex.query client fetch superseded by server-side fetchQuery), use-vehicle-details.ts (sequential useQuery waterfall feeding an as any fabricated vehicle into pricing), use-homepage-featured-vehicles.ts, lib/flight-validation.ts (all exports unimported), and lib/google.ts (unused 'use server' Places autocomplete that console.logs raw responses). Unused dependencies @clerk/react@^6 (a legacy Clerk runtime next to @clerk/nextjs v7) and @googlemaps/google-maps-services-js can be dropped from package.json along with the files. - hooks/use-vehicle-list.ts, hooks/use-vehicle-details.ts, hooks/use-homepage-featured-vehicles.ts, lib/flight-validation.ts, lib/google.ts, package.json
  • duplication - A local getStatusBadge/getStatusColor/getStatusLabel helper with an inline statusConfig map and silent fallback is copy-pasted across at least 6 files (admin transfers page, admin reservation-table, user-reservations-table, both confirmation pages, vehicles-table) with drifting configs — hardcoded English vs i18n labels, the reservation confirmation omitting the 'completed' status. Extract one canonical translated StatusBadge component/typed status map and delete the copies (lib/reservation-utils.ts holds a dead untranslated variant). - app/admin/transfers/page.tsx, components/admin/reservations/reservation-table.tsx, components/features/reservations/user-reservations-table.tsx, app/[locale]/reservation/confirmation/page.tsx, app/[locale]/transfers/confirmation/[transferId]/page.tsx, components/admin/vehicles/vehicles-table.tsx, lib/reservation-utils.ts
  • duplication - formatDate/formatPrice helpers are copy-pasted across three table files with diverged output: prices render as "€X.XX" in admin transfers vs "X.XX EUR" in the two reservation tables, and dates use date-fns format("MMM d, yyyy") vs locale-dependent toLocaleDateString(). Extract shared currency/date formatters into lib. - app/admin/transfers/page.tsx, components/features/reservations/user-reservations-table.tsx, components/admin/reservations/reservation-table.tsx
  • duplication - transfers/vehicles/page.tsx and transfers/booking/page.tsx each re-implement the same guard: load transferStorage in an effect, track isHydrated, render a spinner, then a near-identical 'Missing Transfer Details' fallback with duplicated required-field checks — booking duplicates the same 4-field check a third time in handleSubmit. Extract a useTransferSearch hook (mirroring hooks/use-vehicle-search.ts) plus one guard component. - app/[locale]/transfers/vehicles/page.tsx, app/[locale]/transfers/booking/page.tsx, lib/transfer-storage.ts, hooks/use-vehicle-search.ts
  • duplication - car-detail-client.tsx hand-rolls searchStorage hydration and persistence (RentalState with isHydrated flag, load-in-effect, save-on-update) duplicating the canonical useVehicleSearch hook used by the home and cars pages, minus its date-adjustment logic. Reuse the hook (or extend it with a bulk-update method) instead of a second divergent copy of the storage lifecycle. - app/[locale]/cars/[slug]/car-detail-client.tsx, hooks/use-vehicle-search.ts
  • duplication - about/page.tsx defines a local sectionAnimationVariants that is a verbatim copy of contactAnimationVariants from lib/animations.ts, and its name collides with the canonical sectionAnimationVariants export other pages import. Delete the local copy and import contactAnimationVariants (not sectionAnimationVariants, whose timings differ) to avoid silently changing the page's animations. - app/[locale]/about/page.tsx, lib/animations.ts
  • duplication - lib/mapbox.ts exports a canonical formatDuration(minutes), but transfer-summary-card.tsx re-implements it locally, transfer-search-form.tsx inlines the same hours/minutes formatting in JSX, and the transfer confirmation page carries a dead local copy that is never called. Import the lib helper everywhere and delete the copies. - components/features/transfers/transfer-summary-card.tsx, components/features/transfers/transfer-search-form.tsx, app/[locale]/transfers/confirmation/[transferId]/page.tsx, lib/mapbox.ts
  • duplication - app/admin/transfers/page.tsx (647 lines) and reservations/page.tsx are near-identical dashboards: same 4-stat-card grid, same BarChart + LineChart cards with identical props, and duplicated multi-card skeleton blocks. Extract shared StatCard/chart-card/DashboardSkeleton components, and move the inlined transfers table into components/admin/transfers/ mirroring ReservationsTable. - app/admin/transfers/page.tsx, app/admin/reservations/page.tsx
  • duplication - VehicleOrderingPage triplicates one pricing-field concept (extra-50km price, transfer base fare, transfer multiplier): three value-string states, three isSaving booleans, three near-identical parse/validate/save handlers hitting the same mutation, and three ~31-line copy-pasted Input+Button cards (~175 lines). Extract a single config-driven PricingField component. - app/admin/vehicles/classes/[classId]/page.tsx
  • duplication - classes/page.tsx and classes/[classId]/page.tsx duplicate the entire dnd-kit sortable scaffold: identical SortableCard wrapper, identical useSensors config, and an identical handleDragEnd (arrayMove, optimistic setItems, index-mapped mutation, revert-on-error with toast). Extract a shared SortableList/useSortableReorder helper. - app/admin/vehicles/classes/page.tsx, app/admin/vehicles/classes/[classId]/page.tsx
  • duplication - All six admin create/edit dialogs re-declare raw Radix Tabs primitives and duplicate the same ~330-char trigger className (~20 usage sites) despite the styled components/ui/tabs.tsx already existing; the duplicated string is an older shadcn style, so migrating slightly (harmlessly) normalizes the visuals. - components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/vehicles/edit-vehicle-dialog.tsx, components/admin/reservations/create-reservation-dialog.tsx, components/admin/reservations/edit-reservation-dialog.tsx, components/admin/blog/create-blog-dialog.tsx, components/admin/blog/edit-blog-dialog.tsx, components/ui/tabs.tsx
  • duplication - Create/edit reservation dialogs are copy-paste siblings whose pricing logic diverged: create computes rental days as a naive Math.ceil millisecond diff (in three places) ignoring pickup/return times while edit uses calculateVehiclePricing with times, so they suggest different prices for the same booking; create also hardcodes additionalCharges: undefined, lacks edit's SCDW auto-calculation, and uses the current seasonal multiplier where edit honors the historical one. Extract a shared reservation-form module (schema + pricing derivation). - components/admin/reservations/create-reservation-dialog.tsx, components/admin/reservations/edit-reservation-dialog.tsx
  • duplication - The handleNumberInput keydown filter is copy-pasted with identical logic in all six admin dialogs, handleIntegerInput is duplicated in both vehicle dialogs, and the availableFeatures list is byte-identical in both vehicle dialogs. Extract into a shared lib module or replace the keydown filters with inputMode/type=number. - components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/vehicles/edit-vehicle-dialog.tsx, components/admin/reservations/create-reservation-dialog.tsx, components/admin/reservations/edit-reservation-dialog.tsx, components/admin/seasons/create-season-dialog.tsx, components/admin/seasons/edit-season-dialog.tsx
  • duplication - The locale projection block with the legacy _ro/_en fallback rule (title/description/readingTime, plus content in getBySlug) is copy-pasted in getAll, getFeatured, getPublished, getBySlug, and again in getAllAdmin, while slug is already factored into the resolveSlug helper. Extract a localizeBlog(blog, locale) helper next to resolveSlug so the fallback rule lives in one place. - convex/blogs.ts
  • duplication - Vehicle literal unions (type, fuelType, transmission, status) are duplicated in schema.ts and five times across vehicles.ts with drift already present: getAll and searchAvailableVehicles accept fuelType 'petrol' not in the schema, and getAllVehiclesWithClasses hand-maintains an ~87-line returns validator including a stale class union for a removed field. The same pattern repeats for status/paymentMethod/customerInfo validators across reservations.ts (paymentMethod inlined 3x), transfers.ts, and emails.ts; only pricingTierValidator is shared. - convex/vehicles.ts, convex/schema.ts, convex/reservations.ts, convex/transfers.ts, convex/emails.ts
  • duplication - calculateVehiclePricing and calculateVehiclePricingWithSeason in lib/vehicle-utils.ts are near-identical ~40-line functions (same guard, day calculation, location fees, null fallback); the seasonal variant defaults the multiplier to 1.0 and subsumes the plain one, whose only live call sites are five in edit-reservation-dialog.tsx (use-vehicle-details.ts holds a dead import). Collapse into one function with a defaulted multiplier. - lib/vehicle-utils.ts, components/admin/reservations/edit-reservation-dialog.tsx, hooks/use-vehicle-details.ts
  • duplication - lib/search-storage.ts and lib/transfer-storage.ts duplicate the localStorage machinery — SSR guard, JSON parse/stringify, Date<->ISO conversion, identical validateAndFixDate, and save/load/clear/updateField. A generic createLocalStorage(key, defaults, dateFields) factory would replace the shared core and prevent drift. - lib/search-storage.ts, lib/transfer-storage.ts
  • duplication - getPaymentMethodLabel is implemented four times (identical copies in lib/email-utils.ts and convex/emails/utils.ts, a drifted switch copy in lib/reservation-utils.ts, plus inline labels in the reservation confirmation page and the paymentMethods array), and the slug validators/generators in lib/blog-utils.ts and lib/vehicle-utils.ts share the identical regex and near-identical slugification chains. Consolidate each into one canonical helper with one label casing. - lib/email-utils.ts, convex/emails/utils.ts, lib/reservation-utils.ts, app/[locale]/reservation/confirmation/page.tsx, lib/blog-utils.ts, lib/vehicle-utils.ts
  • duplication - hooks/use-vehicle-filters.ts (300 lines) stamps the same code out four times — four structurally identical option-count memos (brand/fuel/transmission/type), four toggle callbacks, four remove callbacks (each duplicating its toggle's active branch), and 4x-repeated active-filter arithmetic — and vehicle-filters.tsx repeats the same four-way chip and checkbox blocks. Parameterizing by a category key collapses the hook to ~80 lines and the UI to a single map; adding a fifth category currently requires ~10 edits. - hooks/use-vehicle-filters.ts, components/features/vehicles/vehicle-filters.tsx
  • i18n - Hand-rolled plural logic appears three times in the reservation page (full words 'zi'/'zile' vs suffix fragments ''/'le'/'s' passed as {plural} params) instead of next-intl ICU plurals, and the terms-and-conditions checkbox duplicates full Romanian and English JSX behind a locale ternary instead of t.rich with link tags; UserReservationsTable mixes t() with hardcoded English ('From:', 'Pickup:', 'SCDW (Zero deductible)', 'Showing X to Y of Z reservations'). - app/[locale]/reservation/page.tsx, components/features/reservations/user-reservations-table.tsx, messages/en.json, messages/ro.json
  • i18n - terms/page.tsx and privacy/page.tsx serve hardcoded Romanian-only content under both /ro and /en while their generateMetadata advertises English titles/descriptions — English visitors get an EN meta title over an all-Romanian legal page. Either localize the content or make the pages explicitly ro-only (canonical to /ro, no en alternate). - app/[locale]/terms/page.tsx, app/[locale]/privacy/page.tsx, lib/metadata.ts
  • i18n - Landing sections bake display content into component code: testimonials-with-marquee ships ~160 lines of Romanian-only DEFAULT_TESTIMONIALS (with remote Unsplash avatars) that are the actual rendered content since the call site never passes a prop — English visitors see Romanian testimonials — while faq.tsx carries a dead English DEFAULT_CAR_RENTAL_FAQS fallback. Move testimonial content to messages/ or a locale-keyed data module and delete the dead FAQ default. - components/features/landing/testimonials-with-marquee.tsx, components/features/landing/faq.tsx, app/[locale]/home-page-client.tsx
  • modernization - Both framer-motion and motion (a thin re-export wrapper) are declared as dependencies, with framer-motion imported in 7 files and motion/react in 2. The lockfile currently dedupes to one runtime copy, but if the semver ranges diverge npm would install a nested second framer-motion and genuinely duplicate the runtime. Consolidate on one package (motion is the maintained name). - package.json, components/ui/animated-background.tsx, components/ui/progressive-carousel.tsx, lib/animations.ts, app/[locale]/about/page.tsx, components/ui/animated-group.tsx, components/features/landing/faq.tsx, components/features/landing/feature-section-with-hover-effects.tsx, components/admin/blog/edit-blog-dialog.tsx, components/admin/blog/create-blog-dialog.tsx
  • performance - Reservation page gates two Convex subscriptions (vehicleClasses.getById and vehicles.getImageUrl) on the result of vehicles.getById via 'skip', so the vehicle image and class data arrive one full round trip after the vehicle — a two-level waterfall in a fully client-rendered 1888-line page. A single server-side query joining vehicle + class + resolved storage URL (the pattern already used in vehicles.ts:697-727), optionally preloaded from a server wrapper, collapses this to one round trip. - app/[locale]/reservation/page.tsx, convex/vehicles.ts, convex/vehicleClasses.ts
  • performance - The reservation confirmation page has a 3-level waterfall: getReservationById -> vehicles.getById (needs reservation.vehicleId) -> vehicles.getImageUrl (needs vehicle.mainImageId), rendering a spinner until reservation AND vehicle resolve sequentially. getReservationById should return the reservation with vehicle and image URL joined server-side. - app/[locale]/reservation/confirmation/page.tsx, convex/reservations.ts
  • performance - The car detail page issues one fetchQuery(api.vehicles.getImageUrl) HTTP round-trip per gallery image plus one for the main image, and generateMetadata independently re-fetches the same vehicle and image URL (fetchQuery results are not deduped) — a car with 10 photos costs ~13 round-trips per render. Add a single getBySlugWithImageUrls Convex query resolving all storage URLs server-side, the pattern getAllVehiclesWithClasses already uses. - app/[locale]/cars/[slug]/page.tsx, convex/vehicles.ts
  • performance - Blog list images are N+1: each BlogCard runs its own useQuery(api.blogs.getImageUrl) (30 posts = 30 subscriptions, cover images absent from SSR HTML, hurting LCP and image SEO; the featured card's priority Image gets its src only after hydration), and BlogListClient immediately re-fetches getAll + getFeatured client-side, duplicating the unbounded list the server just fetched while the paginated getPublished query goes unused. Resolve coverImageUrl inside getAll/getFeatured via ctx.storage.getUrl and drop the per-card queries and redundant live re-fetch. - components/features/blog/blog-card.tsx, components/features/blog/blog-list-client.tsx, convex/blogs.ts, app/[locale]/blog/page.tsx
  • performance - getAllVehiclesWithClasses collects the whole vehicles table and returns full documents (features, raw storage ids, transfer fields) to every public visitor, and the homepage featured-cars fallback fetches plus URL-signs the entire fleet just to .slice(0, 3) — every homepage request without featured cars pays the full-fleet query for 3 cards. Trim the projection and add a limited 3-vehicle variant for the homepage. - convex/vehicles.ts, app/[locale]/page.tsx, app/[locale]/cars/page.tsx
  • performance - The comment-marked-deprecated vehicles.getAllVehicles (unbounded .collect() of full documents) is still used at 7 call sites: app/sitemap.ts and cars/[slug] generateStaticParams need only {_id, slug, _creationTime}; app/admin/page.tsx subscribes to the whole table for a count; and both reservation dialogs, the admin vehicles table, and the vehicle-classes page subscribe to full docs they render as make/model/year. Add a slim slugs-only query for sitemap/static params and projected/paginated queries for the admin consumers. - convex/vehicles.ts, app/sitemap.ts, app/[locale]/cars/[slug]/page.tsx, app/admin/page.tsx, components/admin/reservations/create-reservation-dialog.tsx, components/admin/reservations/edit-reservation-dialog.tsx, components/admin/vehicles/vehicles-table.tsx, app/admin/vehicles/classes/page.tsx
  • performance - Per-image Convex subscriptions instead of batched URL resolution across admin surfaces: VehicleImage in vehicles-table.tsx (also defined inside the parent body, remounting per render), FeaturedCarSlot in featured-cars-management.tsx, VehicleCardWithImage in the class-ordering page, and ImageThumbnail in both blog dialogs each call useQuery(getImageUrl) per item; duplicate getImageUrl queries also exist in both blogs.ts and vehicles.ts. Resolve imageUrl server-side in the list queries or add a batch getImageUrls query accepting an array of storage IDs. - components/admin/vehicles/vehicles-table.tsx, components/admin/vehicles/featured-cars-management.tsx, app/admin/vehicles/classes/[classId]/page.tsx, components/admin/blog/create-blog-dialog.tsx, components/admin/blog/edit-blog-dialog.tsx, convex/vehicles.ts, convex/blogs.ts
  • performance - Dialogs mounted eagerly while closed keep live Convex subscriptions running: CreateReservationDialog (always rendered on /admin/reservations) subscribes to getAllVehicles plus seasons.getActive/getCurrent via useDateBasedSeasonalPricing; CreateVehicleDialog subscribes to vehicleClasses.list; TransferPricingDialog subscribes to listTiers with activeOnly:false. Gate the queries with 'skip' when !open, or conditionally mount the dialogs. - components/admin/reservations/create-reservation-dialog.tsx, components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/transfers/transfer-pricing-dialog.tsx, hooks/use-date-based-seasonal-pricing.ts, app/admin/reservations/page.tsx, app/admin/vehicles/page.tsx, app/admin/transfers/page.tsx
  • performance - Blog dialogs re-render the entire ~850-line component per keystroke: getLocaleStatus() in the DialogHeader calls form.watch on 8 fields, and the Preview tab's inline form.watch calls are evaluated eagerly in the parent's JSX regardless of active tab — typing in the 14-row MDX textarea re-renders the whole dialog including the framer-motion AnimatePresence tree per character. Move watch-dependent UI into useWatch subcomponents, covering both the LangToggle status and the preview. - components/admin/blog/create-blog-dialog.tsx, components/admin/blog/edit-blog-dialog.tsx
  • performance - blog-content.tsx runs MDX compilation in the browser: next-mdx-remote/serialize plus rehype-highlight (bundling lowlight/highlight.js) execute client-side in a useEffect with a 'Loading content...' flash, and it is re-exported from the blog barrel that public blog client components import from, risking the MDX toolchain leaking into public bundles. A server variant (blog-content-server.tsx) already exists — restrict the client version to the admin preview and import it directly, not via the barrel. - components/features/blog/blog-content.tsx, components/features/blog/index.ts, components/features/blog/blog-preview.tsx, components/features/blog/blog-list-client.tsx, components/features/blog/blog-detail-client.tsx
  • performance - The homepage testimonials marquee renders 19 hardcoded testimonials duplicated x2 = 38 cards in a permanently-running 60s CSS marquee, each with a radix AvatarImage rendering a raw from images.unsplash.com fetched eagerly (radix preloads via a JS Image() object) — ~17 unique third-party image requests on landing with no lazy loading or next/image optimization. Use next/image at a fixed 48px size or a plain lazy ; loading='lazy' on the radix component may not defer the JS preload. - components/features/landing/testimonials-with-marquee.tsx, components/ui/testimonial-card.tsx, components/ui/avatar.tsx
  • performance - HomePageClient lifts searchState to the root of the homepage tree, so every search-form change (location/date/time selection) re-renders the entire page: the slideshow, the 38-card testimonial marquee, FAQ, features, story, CTA, and the unmemoized vehicle card grid, though only the form and featured grid consume searchState. Isolate them into their own stateful subtree or wrap the heavy static sections in React.memo. - app/[locale]/home-page-client.tsx, components/features/vehicles/vehicle-card-with-preloaded-image.tsx, hooks/use-vehicle-search.ts
  • spaghetti - calculateTotalPrice in the reservation page returns a 19-field object with two hand-maintained all-zero fallback literals and runs unmemoized every render, and handleSendReservation then recomputes warranty/scdw/protection/deductible via a different path using a different per-day base for SCDW (getBasePricePerDay*multiplier vs duration-tiered seasonalPricePerDay), so displayed SCDW and persisted protectionCost can disagree. Collapse into one memoized pricing hook used by both the summary UI and the submit payload. - app/[locale]/reservation/page.tsx
  • spaghetti - The cars page filter pipeline does dead work: filter results are pushed to the parent via a useEffect (render -> effect -> parent setState -> full second render of all cards per toggle), cars-page-client duplicates them into displayedVehicles state, and handleFilterChange re-joins with initialVehicles.find() inside .map() (O(n*m)) plus an as VehicleWithImageUrl cast — pure compensation for a non-generic prop signature, since Array.filter preserves object references and the fields are never stripped at runtime; the brand predicate also re-runs normalizeString per vehicle per selected brand. Make useVehicleFilters generic/called directly in the parent, render derived data without the effect round-trip, and pre-normalize brands. - app/[locale]/cars/cars-page-client.tsx, components/features/vehicles/vehicle-filters.tsx, hooks/use-vehicle-filters.ts
  • spaghetti - AdminLayout builds breadcrumbs from path segments with a magic special case: pathSegments.includes("classes") plus a hardcoded index === 3 to swap in the vehicle-class display name, silently coupling the shared layout to one route's URL depth — it breaks the moment any other dynamic segment is added. Move label resolution to a route-config map or per-segment resolver. - app/admin/layout.tsx
  • spaghetti - Vehicle dialogs keep pricingTiers and season dialogs keep periods as useState arrays manually mirrored into the form via form.setValue on every change, with onSubmit reading the state variable rather than the validated form value — two sources of truth that can drift (edit-vehicle's form.reset even sets pricingTiers: [] then patches it afterwards). edit-reservation-dialog already shows the right pattern with useFieldArray; use it in all four dialogs and delete the shadow state. - components/admin/vehicles/create-vehicle-dialog.tsx, components/admin/vehicles/edit-vehicle-dialog.tsx, components/admin/seasons/create-season-dialog.tsx, components/admin/seasons/edit-season-dialog.tsx
  • spaghetti - The 'strip undefined then patch' idiom is re-implemented five different ways across Convex mutations: 18 sequential if-checks in reservations.updateReservationDetails, a Record loop in transfers.updateTransferDetails, per-field ifs in users/vehicleClasses/transferPricing, and seasons.update's Object.fromEntries filter which is miswritten — .filter(([value]) => ...) destructures the KEY as value, making it a no-op (latent, since Convex omits unsupplied optional args). One shared stripUndefined helper replaces all five variants. - convex/seasons.ts, convex/reservations.ts, convex/transfers.ts, convex/users.ts, convex/vehicleClasses.ts, convex/transferPricing.ts
  • spaghetti - rental-details.tsx mirrors all 6 props into 6 local useState hooks kept in sync by 6 one-liner useEffects, then writes every change to both local state and the parent — a fully controlled component simulating uncontrolled state. Rendering directly from props and calling onUpdateDetails on change removes 12 hooks and the sync-drift risk. - components/shared/navigation/rental-details.tsx
  • type-safety - Checkout submit paths are stringly typed: paymentMethod is held as plain string and cast to the union at mutation time in both the reservation page and transfers booking page (the union is duplicated across the zod schema, the paymentMethods config, and the casts), selectedVehicleId is cast via as Id<"vehicles"> in transfer-storage consumers, the seasonId cast papers over lib/season-utils.ts typing season ids as plain string, and the created?.reservationId ?? created unwrap is dead code since createReservation declares a returns validator. Derive the types once (from the config / Id types) and drop the casts. - app/[locale]/reservation/page.tsx, app/[locale]/transfers/booking/page.tsx, lib/transfer-storage.ts, lib/season-utils.ts
  • type-safety - The locale contract is stringly-typed and duplicated: const locales = ['ro','en'] exists in both i18n.ts (unexported) and app/[locale]/layout.tsx, each with an includes(locale as any) check, and unvalidated locale as "ro" | "en" casts appear in the blog server pages and client components. Export a single locales const and Locale type from i18n.ts with a narrowing helper. - app/[locale]/layout.tsx, i18n.ts, app/[locale]/blog/page.tsx, app/[locale]/blog/[slug]/page.tsx, components/features/blog/blog-detail-client.tsx, components/features/blog/blog-list-client.tsx
  • type-safety - Fuel-type values are inconsistent across the codebase: edit-vehicle-dialog's zod schema allows 'petrol' and falls back to it for vehicles with undefined fuelType even though its Select only offers diesel/electric/hybrid/benzina and the Convex schema excludes 'petrol' — saving such a vehicle untouched is rejected by the updateVehicle validator at runtime; 'petrol' also lingers in vehicles.ts's list-filter validator and types/email.ts. Consolidate on the canonical FuelType from types/vehicle.ts and reconcile petrol vs benzina. - components/admin/vehicles/edit-vehicle-dialog.tsx, components/admin/vehicles/create-vehicle-dialog.tsx, convex/vehicles.ts, types/email.ts
  • wrong-layer - app/[locale]/transfers/page.tsx, an indexed SEO landing page, is entirely 'use client' though only TransferSearchForm needs interactivity: all copy and the transferPage messages ship in the client bundle (with framer-motion via AnimatedGroup), and the SSR'd text stays at opacity 0 until hydration because of AnimatedGroup's initial='hidden'. Make the page a server component using getTranslations, keeping the form and AnimatedGroup as client leaves. - app/[locale]/transfers/page.tsx, app/[locale]/transfers/layout.tsx, components/ui/animated-group.tsx
  • wrong-layer - types/vehicle.ts mixes concerns: it correctly derives Vehicle types from the Convex Doc but then hosts ~80 lines of pricing business logic (getBasePriceTier, getPriceForDuration, getTotalPrice, getPriceRange), with getBasePricePerDay throwing on missing tiers — making 'types' a runtime failure source while lib/vehicle-utils.ts is the established home for pricing helpers. Move the functions into lib/vehicle-utils.ts. - types/vehicle.ts, lib/vehicle-utils.ts

Low (14)

  • bug - reservation page.tsx line 538 contains a committed AI tool-call artifact inside a comment: // Form validation using Zod</parameter> — harmless at runtime but leftover generated-output markup signaling the file was committed without review; remove it. - app/[locale]/reservation/page.tsx
  • bug - formProgress in the reservation page declares totalRequiredFields = 9 but actually counts 11 fields and divides by totalRequiredFields + 2 — a magic-number patch, so adding/removing a field silently skews the progress bar. The dead useReservationForm hook already has the cleaner list-based version; use that shape. - app/[locale]/reservation/page.tsx, hooks/use-reservation-form.ts
  • convex-convention - getCurrentUserReservations returns all of a user's reservations as full docs via unbounded .collect() (including customerInfo and additionalCharges), and UserReservationsTable paginates them client-side with useMemo + slice, shipping undisplayed fields and unshown pages; the project's own Convex guidelines require .take()/.paginate(). - convex/reservations.ts, components/features/reservations/user-reservations-table.tsx
  • dead-code - car-detail-client.tsx has an unreachable if (!vehicle) not-found branch (vehicle is a required non-null prop, the server page already calls notFound(), and earlier lines dereference vehicle before the guard), and buildReservationUrl is a useCallback only invoked inline once per render — inline it as a plain template string. - app/[locale]/cars/[slug]/car-detail-client.tsx
  • dead-code - The admin Settings surface is dead weight: the sidebar nav item is commented out in admin-sidebar.tsx while app/admin/settings/page.tsx still ships a placeholder ('Admin settings interface will go here') reachable only by typing the URL. Either delete both or implement the page. - components/admin/admin-sidebar.tsx, app/admin/settings/page.tsx
  • duplication - The documented project-wide default pickup/return time '10:00' is hardcoded independently in lib/search-storage.ts (private DEFAULT_TIME constant) and the form defaultValues of both admin reservation dialogs; the unexported DEFAULT_LOCATION/DEFAULT_TIME constants force the duplication. Export them (or move to a shared constants module) and reuse. - lib/search-storage.ts, components/admin/reservations/create-reservation-dialog.tsx, components/admin/reservations/edit-reservation-dialog.tsx
  • modernization - dev and build scripts force the legacy bundler with next dev/build --webpack on Next 16, where Turbopack is the default and substantially faster (and reactCompiler runs through Babel under webpack). The likely blocker is the MDX rehype/remark plugin functions in next.config.ts, which Next supports specifying as string names for Turbopack compatibility — migrate the MDX config and drop --webpack. - package.json, next.config.ts
  • performance - featuredCars.getFeaturedCars does sequential awaited ctx.db.get calls in a for-loop (sibling getFeaturedVehicles already uses Promise.all; impact bounded to 3 slots), and getAvailableVehiclesForFeatured collects the entire vehicles table returning full documents for dropdowns that render only make/model/year, while the same admin page also subscribes to getAllVehicles via VehiclesTable — duplicating the fleet fetch. Parallelize the gets and return a projected {_id, make, model, year} list. - convex/featuredCars.ts, components/admin/vehicles/featured-cars-management.tsx, components/admin/vehicles/vehicles-table.tsx, app/admin/vehicles/page.tsx
  • performance - blogs.getAllAdmin does an unbounded .collect() of the blogs table and BlogTable renders every row with no pagination at all — unlike the vehicles/reservations tables there is no ITEMS_PER_PAGE slicing, so DOM size and payload grow with every published post. Add pagination (Convex .paginate or at minimum client-side slicing). - convex/blogs.ts, components/admin/blog/blog-table.tsx, app/admin/blogs/page.tsx
  • performance - mapbox-location-search.tsx debounces suggest requests but never aborts in-flight fetches and has no stale-response guard, so fast typing over slow network can resolve out of order and display suggestions for an older query; setLoading(true) also fires synchronously per keystroke before the 200ms debounce. Add an AbortController in the effect cleanup (or a request-id stale check) and set loading inside the debounced callback. - components/shared/search-filters/mapbox-location-search.tsx
  • performance - transfer-search-form.tsx always re-fires the Mapbox Directions API call on mount when pickup/dropoff are restored from localStorage, even though distanceKm and estimatedDurationMinutes were just restored from the same storage — one redundant paid API round-trip per page view. Skip recalculation when stored route info exists for the same location pair. - components/features/transfers/transfer-search-form.tsx
  • performance - All form fields in the 588-line transfers/booking/page.tsx live in one component's state, so every keystroke re-renders the entire page tree including the unmemoized TransferSummaryCard sidebar; the vehicle prop is a new object literal built inline each render, so React.memo alone is insufficient — memoize the vehicle object (or pass primitives) or extract the personal-info form into its own component. - app/[locale]/transfers/booking/page.tsx, components/features/transfers/transfer-summary-card.tsx
  • performance - app/[locale]/layout.tsx loads the full message catalog via getMessages() (~10KB gzipped, 26 namespaces) and passes it unfiltered into NextIntlClientProvider, so every hard navigation of any public route serializes all namespaces into the RSC payload even though each route's client components use only a few. Pick only the namespaces needed per route and rely on server-side getTranslations for the rest. - app/[locale]/layout.tsx, app/providers.tsx, messages/ro.json, messages/en.json
  • type-safety - zodResolver(schema) as any with an eslint-disable appears three times (transfer-pricing-dialog twice, create-class-dialog once) to paper over the z.coerce input/output mismatch — the correct fix is typing the form with z.input or useForm<Input, unknown, Output>. Meanwhile other dialogs avoid coercion by modeling numeric fields as regex-validated strings plus manual parseInt at submit — two competing conventions; standardize on typed coercion. - components/admin/transfers/transfer-pricing-dialog.tsx, components/admin/vehicle-classes/create-class-dialog.tsx