Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughAdds full i18n support (en, es, zh, hi, ar), a filesystem-backed MDX blog, multiple marketing/legal pages, locale-aware routing/layouts, translated message catalogs, contact form/server action, various marketing UI components and accessibility updates, sitemap/i18n middleware integration, tests, and multiple config/asset updates. Changes
Sequence Diagram(s)sequenceDiagram
participant Browser
participant Proxy as Proxy/Middleware
participant Intl as next-intl Middleware
participant LocaleLayout as LocaleLayout
participant Messages as Message Loader
participant Page as Page Component
Browser->>Proxy: GET /es/blog
Proxy->>Intl: intlMiddleware(req)
Intl->>Proxy: Response (with locale set)
Proxy->>LocaleLayout: render with locale
LocaleLayout->>Messages: import messages for 'es'
Messages->>LocaleLayout: messages JSON
LocaleLayout->>Page: provide NextIntlClientProvider
Page->>Browser: Render localized page
sequenceDiagram
participant User
participant BlogPage
participant ContentLib
participant FS as Filesystem
participant MDX as MDXRemote
User->>BlogPage: Navigate /blog/decentralized-mesh
BlogPage->>ContentLib: getPostBySlug('decentralized-mesh')
ContentLib->>FS: read file content
FS->>ContentLib: file data
ContentLib->>BlogPage: {meta, content}
BlogPage->>MDX: render content
MDX->>User: Rendered HTML
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
Warning Review the following alerts detected in dependencies. According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.
|
There was a problem hiding this comment.
Pull request overview
This PR significantly expands the marketing site by adding locale-aware routing (next-intl), a new blog backed by MDX content, and several new/updated marketing sections, while also tightening SEO metadata and edge security header handling.
Changes:
- Add next-intl locale routing + message catalogs (en/es/zh/hi/ar) and refactor the app layout structure around
[locale]. - Introduce a new MDX-powered blog (content loader + blog index + blog post pages) and add legal pages (Terms/Privacy).
- Update marketing UI components/sections (new sections, accessibility improvements) and add/expand unit tests for utilities.
Reviewed changes
Copilot reviewed 73 out of 90 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| test.ts | Adds an ad-hoc Sentry test snippet (appears non-production). |
| src/utils/seo/speculation-schema.test.ts | Adds tests for schema graph helpers. |
| src/utils/seo/meta.ts | Updates metadata/title behavior, OG image title, Twitter fields, viewport theme colors. |
| src/utils/seo/meta.test.ts | Adds tests for metadata + viewport helpers. |
| src/utils/logger.test.ts | Adds Logger unit tests (bun:test). |
| src/utils/helpers.ts | Refactors URL helper and adds hash/scroll centering helpers with SSR guards. |
| src/utils/helpers.test.ts | Adds tests for helpers (URL/hash/scroll + stringify). |
| src/styles/globals.css | Updates tokens, adds popover vars and locale font overrides, sets color-scheme. |
| src/proxy.ts | Composes next-intl middleware with security headers/CSP and updates matcher. |
| src/mdx-components.tsx | Adds MDX components passthrough hook. |
| src/lib/utils.test.ts | Adds tests for cn() utility. |
| src/lib/content.ts | Adds MDX post loader (gray-matter + filesystem). |
| src/i18n/routing.ts | Defines locale routing configuration. |
| src/i18n/request.ts | Loads locale messages via next-intl server config. |
| src/i18n/navigation.ts | Adds locale-aware navigation helpers (currently unused). |
| src/hooks/index.ts | Normalizes export quoting/style. |
| src/features/marketing/sections/why-now.tsx | Accessibility tweaks (aria-labelledby, icon aria-hidden) + styling tweaks. |
| src/features/marketing/sections/use-cases.tsx | Adds aria-labelledby and refines Tabs UI + responsive label behavior. |
| src/features/marketing/sections/trust-security.tsx | New marketing section. |
| src/features/marketing/sections/technology.tsx | Adds aria-labelledby + visual emphasis tweak. |
| src/features/marketing/sections/problem-statement.tsx | Adds aria-labelledby, reduced-motion pulse, table labeling and layout tweaks. |
| src/features/marketing/sections/platform-pillars.tsx | Adds aria-labelledby + icon aria-hidden and styling tweaks. |
| src/features/marketing/sections/metrics-strip.tsx | New metrics strip section. |
| src/features/marketing/sections/index.ts | Exports new sections. |
| src/features/marketing/sections/how-it-works.tsx | Adds aria-labelledby. |
| src/features/marketing/sections/hero.tsx | Makes hero slideshow drive HUD scenario; adds aria-labelledby and styling tweaks. |
| src/features/marketing/sections/footer-cta.tsx | Replaces button CTA with inline mailto email form + a11y improvements. |
| src/features/marketing/sections/competitive-table.tsx | Adds aria-labelledby + layout tweaks. |
| src/features/marketing/sections/architecture-diagram.tsx | Adds aria-labelledby + table min-width tweak. |
| src/features/marketing/components/navbar.tsx | Refactors nav items to use translations + improves a11y/mobile behavior. |
| src/features/marketing/components/hero-video-slideshow.tsx | Adds onIndexChange callback and adjusts video rendering. |
| src/features/marketing/components/hero-hud.tsx | Makes HUD scenario-driven and reduced-motion friendly. |
| src/features/marketing/components/hash-scroll-center.tsx | New client helper to center-scroll known hash sections. |
| src/features/marketing/components/footer.tsx | Refactors footer for translations and fetches /api/status for live status. |
| src/features/marketing/components/contact-form.tsx | New TanStack React Form contact form using a safe server action. |
| src/env.ts | Extends env schema (SENTRY_URL), exports inferred ProcessEnv typing. |
| src/constants/app.ts | Expands app metadata (name/desc/email/links/socials/company). |
| src/config/config.ts | Extends runtime config (links/socials/company/betterstack) and adjusts Vercel env source. |
| src/app/sitemap.ts | Replaces static route list with route config (priority/changefreq). |
| src/app/manifest.ts | Updates manifest “blog” entry to new blog naming/URL. |
| src/app/layout.tsx | Refactors root layout to only return children; viewport remains. |
| src/app/global-error.tsx | Adds dark class and improves semantics (main role=alert). |
| src/app/api/[[...route]]/health/handlers.ts | Removes redundant @version tags in docs. |
| src/app/[locale]/not-found.tsx | Adds metadata and minor layout/icon a11y tweaks. |
| src/app/[locale]/loading.tsx | Adds reduced-motion handling + aria-busy/progressbar semantics and styling tweaks. |
| src/app/[locale]/layout.tsx | New locale layout: fonts, direction (rtl for ar), messages provider, static params. |
| src/app/[locale]/forbidden.tsx | Removes per-page metadata construction. |
| src/app/[locale]/error.tsx | Improves typing + semantics (main role=alert) and import ordering. |
| src/app/[locale]/(marketing)/protocol/page.tsx | New Protocol marketing page with metadata. |
| src/app/[locale]/(marketing)/platform/page.tsx | New Platform marketing page with metadata. |
| src/app/[locale]/(marketing)/page.tsx | Updates home composition (adds MetricsStrip, removes some sections). |
| src/app/[locale]/(marketing)/legal/terms/page.tsx | New Terms page with metadata. |
| src/app/[locale]/(marketing)/legal/privacy/page.tsx | New Privacy page with metadata. |
| src/app/[locale]/(marketing)/legal/layout.tsx | New legal layout wrapper. |
| src/app/[locale]/(marketing)/layout.tsx | New marketing layout (navbar/footer/hash scroll + skip link + main wrapper). |
| src/app/[locale]/(marketing)/contact/page.tsx | New Contact page integrating ContactForm + external resource links. |
| src/app/[locale]/(marketing)/blog/page.tsx | New Blog index page listing MDX posts. |
| src/app/[locale]/(marketing)/blog/[slug]/page.tsx | New Blog post page rendering MDX + dynamic metadata. |
| src/app/(marketing)/layout.tsx | Removes old non-locale marketing layout. |
| src/actions/contact/submit.ts | Adds new server action for contact submission (zod validated). |
| src/actions/contact/submit-contact.ts | Removes prior server action implementation. |
| public/sitemap.xml | Removes static sitemap file (replaced by Next sitemap route). |
| public/assets/videos/drone-landing-video.mp4 | Adds marketing video asset. |
| public/assets/videos/Futuristic_Drone_Lands_On_Logo.mp4 | Adds marketing video asset. |
| public/assets/images/traffic-violation.webp | Adds marketing image asset. |
| public/assets/images/traffic-violation.jpeg | Adds marketing image asset. |
| public/assets/images/swat-and-technical.webp | Adds marketing image asset. |
| public/assets/images/swat-and-technical.jpeg | Adds marketing image asset. |
| public/assets/images/natural-disasters.webp | Adds marketing image asset. |
| public/assets/images/natural-disasters.jpeg | Adds marketing image asset. |
| public/assets/images/mass-casualty-incident.webp | Adds marketing image asset. |
| public/assets/images/mass-casualty-incident.jpeg | Adds marketing image asset. |
| public/assets/images/car-crashes.webp | Adds marketing image asset. |
| public/assets/images/car-crashes.jpeg | Adds marketing image asset. |
| public/assets/images/active-fires.webp | Adds marketing image asset. |
| public/assets/images/active-fires.jpeg | Adds marketing image asset. |
| package.json | Adds deps for forms, MDX, next-intl, gray-matter, critters, bun types. |
| next.config.ts | Adds next-intl plugin integration; adjusts redirects and Sentry build config. |
| messages/zh.json | Adds Chinese translations. |
| messages/hi.json | Adds Hindi translations. |
| messages/es.json | Adds Spanish translations. |
| messages/en.json | Adds English translations. |
| messages/ar.json | Adds Arabic translations. |
| instrumentation-client.ts | Adjusts Sentry client setup and exports router transition instrumentation. |
| env.d.ts | Replaces manual NODE_ENV typing with inferred env contract. |
| content/posts/introducing-resq-tactical-os.mdx | Adds MDX blog content. |
| content/posts/decentralized-mesh-networks-save-lives.mdx | Adds MDX blog content. |
| content/posts/building-for-zero-connectivity.mdx | Adds MDX blog content. |
| .gitignore | Adds additional ignored directories/files. |
Comments suppressed due to low confidence (1)
src/proxy.ts:100
- CSP
script-srcincludes a per-request nonce +strict-dynamic, but the nonce is no longer attached to the request (it’s only set as a response header). Without propagating the nonce into the rendering pipeline (e.g., viaNextResponse.next({ request: { headers }})or another supported Next nonce mechanism), Next.js inline scripts won’t receive the nonce and may be blocked in production. Please restore request-header propagation (and ensure it composes correctly withintlMiddleware) or relax the CSP accordingly.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| <NavigationMenuContent className="overflow-visible!"> | ||
| <ul className="grid w-[min(400px,90vw)] gap-1 p-2 md:w-112.5 md:grid-cols-2"> | ||
| {platformItems.map((item) => ( |
There was a problem hiding this comment.
className="overflow-visible!" looks like an invalid Tailwind “important” modifier (Tailwind uses !overflow-visible, not a trailing !). As written, the class will likely be ignored and the menu content may still be clipped. Please replace with the correct utility (or an explicit style) so the dropdown can overflow as intended.
| <NavigationMenuContent className="overflow-visible!"> | ||
| <ul className="grid w-[min(400px,90vw)] gap-1 p-2 md:w-125 md:grid-cols-2"> | ||
| {developerItems.map((item) => ( |
There was a problem hiding this comment.
Same issue here: className="overflow-visible!" is likely not a valid Tailwind important modifier and may not apply. Consider switching to !overflow-visible (or another supported approach) to avoid dropdown clipping.
| import { createNavigation } from "next-intl/navigation"; | ||
| import { routing } from "./routing"; | ||
|
|
||
| export const { Link, redirect, usePathname, useRouter, getPathname } = | ||
| createNavigation(routing); |
There was a problem hiding this comment.
This locale-aware navigation wrapper isn’t referenced anywhere else in the repo (no imports found). If it’s not going to be used, consider removing it to avoid dead code; otherwise, update internal navigation to consume these helpers so locale prefixing is consistent across the app.
test.ts
Outdated
| import * as Sentry from "@sentry/browser"; | ||
| (() => console.log(Sentry.captureMessage("Hello Better Stack, this is a test message from JavaScript!")))(); No newline at end of file |
There was a problem hiding this comment.
This file looks like an ad-hoc Sentry smoke-test (imports @sentry/browser and immediately captures/logs a message). If it’s not part of the shipped app/test suite, it should be removed or moved to a dedicated script/examples area to avoid accidental execution/packaging in CI or production builds.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly upgrades the application's infrastructure by integrating robust internationalization capabilities and a flexible blog content management system. It also includes crucial updates to environment variable handling, Sentry monitoring, and introduces new legal and contact pages. These changes, coupled with various UI/UX enhancements and dependency updates, aim to improve the application's global reach, content delivery, maintainability, and overall user experience. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Actionable comments posted: 13
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (8)
src/app/[locale]/loading.tsx (1)
6-13:⚠️ Potential issue | 🟠 MajorLocalize hardcoded loading copy in this locale route.
Strings are currently hardcoded in English inside
src/app/[locale]/..., including screen-reader text. This will render English UI in non-English locales.Suggested direction
-const bootSequence = [ - "INITIATING KINETIC COMMAND...", - ... - "[OK] SYSTEM READY." -]; +// Pull localized strings from your locale dictionary/messages source +const bootSequence = messages.loading.bootSequence; -aria-label="Application loading" +aria-label={messages.loading.ariaLabel} -<span>Boot Sequence</span> +<span>{messages.loading.bootSequenceLabel}</span>Also applies to: 56-57, 86-87
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/loading.tsx around lines 6 - 13, The bootSequence array and other hardcoded strings in src/app/[locale]/loading.tsx (e.g., the bootSequence constant and the screen-reader text at the other occurrences noted) must be replaced with localized strings; extract each hardcoded message into your i18n/message catalog (or use the existing getDictionary/getTranslator for this route), then replace bootSequence with an array of translated entries (e.g., t('loading.boot.init') etc.) and replace any aria/live or screen-reader text with translator calls as well; ensure you load the correct locale (using the existing locale route helper) and fallback safely if a key is missing.src/app/global-error.tsx (1)
1-10:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
As per coding guidelines, all source files must include the Apache-2.0 license header.
📄 Add license header
"use client"; +// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + /** * Renders the application-wide error boundary when the root layout fails. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/global-error.tsx` around lines 1 - 10, This file (global-error.tsx) is missing the required Apache-2.0 license header; add the standard Apache-2.0 license comment block at the very top of the file (above the "use client" directive) so it precedes all code and imports (e.g., the "use client" line and imports of Sentry, useEffect, Button) to comply with project licensing guidelines.src/constants/app.ts (1)
1-6:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
Per coding guidelines, all source files must include the Apache-2.0 license header.
Proposed fix
+// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + /** * Declares static application metadata that is safe to embed across server and * client code. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/constants/app.ts` around lines 1 - 6, This file is missing the required Apache-2.0 license header; add the standard Apache-2.0 license header comment block at the very top of src/constants/app.ts (above the existing file comment and the import of packageJson) so the file begins with the full license header before the current module-level comment and the import statement; ensure the header is the canonical Apache-2.0 text used across the repo.src/app/sitemap.ts (1)
1-6:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
Per coding guidelines, all source files must include the Apache-2.0 license header. Add the standard header at the top of the file.
Proposed fix
+// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + /** * Generates the static sitemap metadata for the marketing routes. */🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/sitemap.ts` around lines 1 - 6, Add the standard Apache-2.0 license header as the first block comment in this file so every source file includes the required license; place it above the existing file comment and imports in src/app/sitemap.ts (i.e., before the /** Generates the static sitemap metadata... */ comment), ensuring the header is the canonical Apache-2.0 text and preserves the existing imports of MetadataRoute and config.src/config/config.ts (1)
1-6:⚠️ Potential issue | 🟡 MinorMissing required Apache-2.0 license header.
Per coding guidelines, all source files in
src/**/*.{ts,tsx}must include the Apache-2.0 license header. Add the license comment at the top of the file.Proposed fix
+// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + /** * Derives runtime-safe application configuration from static constants and the * validated environment layer.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/config/config.ts` around lines 1 - 6, Add the required Apache-2.0 license header comment to the very top of this source file (above the existing JSDoc and imports) so the file complies with project guidelines; ensure the standard Apache-2.0 text block is used and placed before the import lines that reference symbols like app and env (i.e., before the lines "import { app } from "@/constants/app";" and "import { env } from "@/env";").src/utils/seo/meta.ts (1)
1-3:⚠️ Potential issue | 🟡 MinorMissing required Apache-2.0 license header.
Per coding guidelines, all source files in
src/**/*.{ts,tsx}must include the Apache-2.0 license header.Proposed fix
+// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + import { app } from '@/constants'; import type { Metadata, Viewport } from 'next';🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/seo/meta.ts` around lines 1 - 3, The file src/utils/seo/meta.ts is missing the required Apache-2.0 license header; add the standard Apache-2.0 license comment block at the very top of the file (before any imports) so all source files under src/**/*.{ts,tsx} comply; ensure the header includes the copyright line and Apache-2.0 notice and leave the rest of the file (imports like app, and types Metadata and Viewport) unchanged.src/features/marketing/components/navbar.tsx (2)
172-186:⚠️ Potential issue | 🟡 MinorReplace
overflow-visible!with!overflow-visiblein two locations (lines 172 and 182).Tailwind CSS v4 uses prefix-based important modifiers, so
overflow-visible!is ignored. Use!overflow-visibleinstead.🔧 Proposed fix
- <NavigationMenuContent className="overflow-visible!"> + <NavigationMenuContent className="!overflow-visible"> @@ - <NavigationMenuContent className="overflow-visible!"> + <NavigationMenuContent className="!overflow-visible">🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/navbar.tsx` around lines 172 - 186, In NavigationMenuContent components inside the navbar (the two occurrences currently using className="overflow-visible!"), replace the invalid Tailwind v4 suffix-style modifier with the prefix-style important modifier by changing those className values to use "!overflow-visible" instead of "overflow-visible!"; locate the two NavigationMenuContent usages near the NavigationMenuItem/NavigationMenuTrigger blocks (the ones around platformItems mapping and the developers section) and update their className strings accordingly.
66-90:⚠️ Potential issue | 🟠 MajorUse the localized Link wrapper for internal routes in NavListItem.
NavListItemalways renders a raw<a>tag, causing internal routes (like/platform,/protocol) to bypass Next.js client-side navigation and prefetching. This also risks bypassing locale-aware routing provided by next-intl.For internal routes, import and use the localized
Linkfrom@/i18n/navigation. Keep raw<a>tags only forexternaltargets:import { Link } from "@/i18n/navigation";Then conditionally render the appropriate element based on the
externalprop.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/navbar.tsx` around lines 66 - 90, NavListItem currently always renders a raw <a> which breaks Next.js client navigation and locale-aware routing for internal routes; update NavListItem to import the localized Link from "@/i18n/navigation" and conditionally render that Link (for internal routes when external is false) instead of the raw <a>, while keeping the existing linkProps and external branch to render a plain <a> for external links; ensure you wrap the chosen element with NavigationMenuLink asChild and pass href, className, linkProps, and the title/description children unchanged.
🟡 Minor comments (11)
src/features/marketing/sections/platform-pillars.tsx-52-54 (1)
52-54:⚠️ Potential issue | 🟡 MinorHeading opacity may reduce contrast below WCAG AA threshold.
The heading text uses
text-text-primary/60(60% opacity), which significantly reduces contrast compared to full opacity. Depending on the--color-text-primaryvalue against--color-bg-primary, this may fall below WCAG AA contrast requirements (4.5:1 for normal text, 3:1 for large text).Consider verifying the computed contrast ratio meets accessibility standards, or use full opacity for the heading text while keeping the emphasis on the span only.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/sections/platform-pillars.tsx` around lines 52 - 54, The heading element with id "platform-heading" uses the utility class text-text-primary/60 which may lower contrast below WCAG AA; update the h2 (platform-heading) to use full text color (remove the /60 opacity) and apply reduced opacity only to any decorative/emphasized span inside the heading (or adjust to a safer token like text-text-primary or text-text-primary/90) so the main heading text retains sufficient contrast against the background; ensure you change the className on the <h2 id="platform-heading"> and move any opacity class to the specific <span> used for emphasis.src/hooks/index.ts-1-1 (1)
1-1:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
As per coding guidelines, all source files in
src/**/*.{ts,tsx,js,jsx}must include the Apache-2.0 license header.Proposed fix
+// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + export * from "./use-in-view";🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/hooks/index.ts` at line 1, Add the standard Apache-2.0 license header to this source file so it complies with project guidelines; update the top of src/hooks/index.ts (above the existing export * from "./use-in-view"; line) to include the full Apache-2.0 license comment block used across the repo so all files matching src/**/*.{ts,tsx,js,jsx} have the required header.src/utils/helpers.ts-117-136 (1)
117-136:⚠️ Potential issue | 🟡 MinorDon't force smooth scrolling for users who requested reduced motion.
globals.cssonly enables smooth scrolling when the user has not asked for reduced motion, but this helper hard-codes"smooth"and overrides that preference for every programmatic scroll.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/helpers.ts` around lines 117 - 136, The helper scrollElementIntoCenterView currently hardcodes smooth scrolling; change it to respect user reduced-motion preferences by removing the hard-coded default and computing an effective behavior: detect prefers-reduced-motion via window.matchMedia('(prefers-reduced-motion: reduce)').matches (guard for typeof window === 'undefined' for SSR), then set effectiveBehavior = behavior ?? (prefersReduced ? 'auto' : 'smooth'), and pass effectiveBehavior to window.scrollTo; update the function signature (scrollElementIntoCenterView) to not force "smooth" and use this computed behavior.src/styles/globals.css-67-83 (1)
67-83:⚠️ Potential issue | 🟡 MinorQuote
SFMono-Regularin the new locale font stacks.The added unquoted token is already tripping Stylelint's
value-keyword-caserule on Lines 70, 76, and 82. Quoting the family name fixes the new lint errors.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/styles/globals.css` around lines 67 - 83, The font-family stacks in the locale-specific :root rules (:root[lang="zh"], :root[lang="hi"], :root[lang="ar"]) use SFMono-Regular unquoted in the --font-data values which triggers Stylelint's value-keyword-case; update each --font-data declaration to wrap SFMono-Regular in quotes (e.g., "SFMono-Regular") so the family name is a valid quoted font-family token while keeping the other fallbacks unchanged.src/utils/helpers.test.ts-148-163 (1)
148-163:⚠️ Potential issue | 🟡 MinorMissing cleanup for
NEXT_PUBLIC_BASE_URLin the finally block.Line 157 deletes
NEXT_PUBLIC_BASE_URLinside the try block, but if an assertion fails before that line, the env variable remains set, potentially polluting subsequent tests.🛡️ Proposed fix to move cleanup to finally block
test("uses env variable as fallback", () => { const original = globalThis.location; + const savedEnv = process.env.NEXT_PUBLIC_BASE_URL; try { // `@ts-expect-error` - testing without location delete globalThis.location; process.env.NEXT_PUBLIC_BASE_URL = "https://env.example.com"; expect(getURL("docs")).toBe("https://env.example.com/docs"); - - delete process.env.NEXT_PUBLIC_BASE_URL; } finally { + if (savedEnv !== undefined) { + process.env.NEXT_PUBLIC_BASE_URL = savedEnv; + } else { + delete process.env.NEXT_PUBLIC_BASE_URL; + } if (original) { globalThis.location = original; } } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/helpers.test.ts` around lines 148 - 163, The test for getURL mutates process.env.NEXT_PUBLIC_BASE_URL inside the try block and deletes it there, which can leave the env var set if an assertion throws; capture the original value (e.g., const originalBase = process.env.NEXT_PUBLIC_BASE_URL) before mutating, then move cleanup into the finally block to restore process.env.NEXT_PUBLIC_BASE_URL to originalBase (or delete it if originalBase was undefined); keep the existing globalThis.location restore logic and ensure both environment and location are always restored in finally.src/app/[locale]/error.tsx-1-5 (1)
1-5:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
This file is missing the required license header. Additionally, the
Buttonimport is placed awkwardly before the JSDoc comment block.🔧 Add license header and reorganize imports
"use client"; - -import { Button } from "@resq-sw/ui/button"; +// Copyright 2025 ResQ Software, Inc. +// SPDX-License-Identifier: Apache-2.0 + /** * Renders the route-level error boundary for recoverable application failures. */ import * as Sentry from "@sentry/nextjs"; import { useEffect } from "react"; +import { Button } from "@resq-sw/ui/button";As per coding guidelines: "All source files must include the Apache-2.0 license header."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/error.tsx around lines 1 - 5, Add the required Apache-2.0 license header at the top of the file and ensure the "use client" directive and imports are placed after the header and any file-level JSDoc; specifically, insert the standard Apache-2.0 license block above the "use client" line, then move the Button import (import { Button } from "@resq-sw/ui/button") to follow the header and JSDoc so the file-level comment and license appear first.src/app/[locale]/(marketing)/layout.tsx-16-21 (1)
16-21:⚠️ Potential issue | 🟡 MinorSkip link text should use i18n translation.
The "Skip to main content" text is hardcoded in English within a locale-aware route. Since this layout is under
[locale], it should use the translated string from the message catalogs (e.g.,Nav.skipToContent).Proposed fix using next-intl
+import { useTranslations } from "next-intl"; + export default function MarketingLayout({ children, }: Readonly<{ children: React.ReactNode; }>) { + const t = useTranslations("Nav"); return ( <> <HashScrollCenter /> <a href="#main-content" className="sr-only focus:not-sr-only focus:fixed focus:top-4 focus:left-4 focus:z-100 focus:rounded-md focus:bg-bg-card focus:px-4 focus:py-2 focus:text-text-primary focus:outline-2 focus:outline-accent-blue" > - Skip to main content + {t("skipToContent")} </a>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/layout.tsx around lines 16 - 21, The skip link text is hardcoded; update the anchor in layout.tsx to use the i18n message key (e.g., Nav.skipToContent) by importing and using next-intl's translations hook (useTranslations) or equivalent, e.g., call const t = useTranslations('Nav') in the layout component and replace the literal "Skip to main content" with t('skipToContent') (or the exact key used in your messages), ensuring the translated string is rendered for the current locale.src/features/marketing/components/contact-form.tsx-26-31 (1)
26-31:⚠️ Potential issue | 🟡 MinorMissing error handling for submission failures.
The form only handles the success case (
result?.data?.success). If the server action fails or returns an error, the user receives no feedback. Consider displaying an error message when the submission fails.Proposed fix
+ const [error, setError] = useState<string | null>(null); + const form = useForm({ defaultValues: { name: "", email: "", organization: "", message: "", }, onSubmit: async ({ value }) => { + setError(null); const result = await executeAsync(value); if (result?.data?.success) { setSubmitted(true); + } else { + setError(result?.data?.message ?? "Submission failed. Please try again."); } }, });Then render the error above the submit button:
{error && ( <p role="alert" className="font-mono text-xs text-accent-red"> {error} </p> )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/contact-form.tsx` around lines 26 - 31, The onSubmit handler (onSubmit, executeAsync, setSubmitted) only handles the success path and ignores failures or thrown exceptions; update onSubmit to catch errors and handle non-success responses by setting an error state (e.g., error via useState) and clearing it on success, and ensure thrown exceptions from executeAsync are caught (try/catch) to set that error state; finally render the error state above the submit button (the suggested <p role="alert" ...>{error}</p> block) so users see failure feedback.src/app/[locale]/(marketing)/blog/page.tsx-17-23 (1)
17-23:⚠️ Potential issue | 🟡 MinorHardcoded locale breaks i18n date formatting.
The
formatDatefunction hardcodes"en-US"locale, but the app supports multiple locales (English, Spanish, Chinese, Hindi, Arabic). Users viewing the blog in other locales will still see English-formatted dates.Consider using the page's locale parameter for proper localization:
💡 Proposed fix to use dynamic locale
-function formatDate(dateStr: string) { - return new Date(dateStr).toLocaleDateString("en-US", { +function formatDate(dateStr: string, locale: string = "en-US") { + return new Date(dateStr).toLocaleDateString(locale, { year: "numeric", month: "long", day: "numeric", }); }Then pass the locale from page params:
export default function BlogPage({ params }: { params: { locale: string } }) { // ... {formatDate(post.date, params.locale)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/blog/page.tsx around lines 17 - 23, The formatDate function currently hardcodes "en-US" which breaks i18n; update formatDate(dateStr: string, locale?: string) to accept a locale parameter and pass that locale to toLocaleDateString instead of the fixed "en-US", then update BlogPage (export default function BlogPage({ params }: { params: { locale: string } })) to call formatDate(post.date, params.locale) when rendering posts so dates are localized per the page locale.src/features/marketing/sections/footer-cta.tsx-46-84 (1)
46-84:⚠️ Potential issue | 🟡 MinorThe "256-BIT SSL" claim is misleading for a mailto: form.
The form uses
action="mailto:..."which simply opens the user's email client rather than submitting data over HTTPS. The "ENCRYPTED TRANSMISSION // 256-BIT SSL" text on line 83 is inaccurate since no server-side encrypted transmission occurs—the email client handles delivery using whatever transport the user's email provider uses.Consider either:
- Removing or rewording the encryption claim to avoid misleading users
- Implementing an actual server-side form submission (via a server action) if encrypted transmission is a requirement
💡 Suggested text change
- ENCRYPTED TRANSMISSION {"// "}256-BIT SSL + DIRECT EMAIL {"// "}OPENS MAIL CLIENT🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/sections/footer-cta.tsx` around lines 46 - 84, The footer form currently uses action="mailto:..." (the <form> element with id/name around the input id="footer-email" and submit <button>), so the adjacent paragraph that reads "ENCRYPTED TRANSMISSION // 256-BIT SSL" is misleading; either remove or reword that <p> (the string "ENCRYPTED TRANSMISSION // 256-BIT SSL") to accurately reflect that the form opens the user’s email client (e.g., "Opens your email client; delivery handled by your email provider") or replace the mailto form with a real server-side submission (implement a server action/POST handler and update the <form> action to point to it) if you require true HTTPS/encrypted transport.src/features/marketing/components/footer.tsx-28-28 (1)
28-28:⚠️ Potential issue | 🟡 MinorLocalize the remaining assistive text in the footer.
The visible copy is translated, but the nav label, homepage/social
aria-labels, the external-link sr-only suffix, andtoLocaleTimeString()are still English/browser-locale only. That leaves screen-reader output and timestamps inconsistent on non-English pages.Also applies to: 91-109, 173-179
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/footer.tsx` at line 28, Localize the remaining assistive text and timestamps in the footer by replacing hard-coded English strings and default locale calls: use the i18n translation function (e.g., t or translate) for the nav label and all aria-labels for homepage/social links and the external-link screen-reader suffix (the bit that renders when external is true), and call toLocaleTimeString with the page locale (e.g., toLocaleTimeString(locale, options)) or format the timestamp via the app's date helper so timestamps respect the current locale; update the components/variables that render external, nav label, homepage/social aria-labels, and the toLocaleTimeString usage accordingly (refer to the external conditional rendering, nav aria-label props, and the timestamp rendering code).
🧹 Nitpick comments (13)
src/app/[locale]/loading.tsx (1)
3-4: Usecn()for conditional class composition.Inline ternary class selection should use the shared
cn()utility for consistency with project styling conventions.Suggested change
import { AnimatePresence, motion, useReducedMotion } from 'motion/react'; import { useEffect, useState } from 'react'; +import { cn } from '@/lib/utils'; ... - className={index === bootSequence.length - 1 ? 'font-bold text-accent-green' : 'text-accent-red'} + className={cn( + index === bootSequence.length - 1 ? 'font-bold text-accent-green' : 'text-accent-red' + )}As per coding guidelines,
src/{app,components,features,styles}/**/*.{ts,tsx,css}: Styling must use Tailwind CSS v4 with configuration in src/styles/globals.css; use cn() utility from src/lib/utils for conditional class composition.Also applies to: 70-70
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/loading.tsx around lines 3 - 4, The component in src/app/[locale]/loading.tsx uses inline ternary expressions for className composition; import the shared cn utility (from src/lib/utils) and replace those inline ternaries (including the one around line 70) with cn(...) calls so classes are composed consistently — update the imports to include cn, then change className={condition ? 'a b' : 'c d'} to className={cn('base-classes', condition && 'conditional-class')} or the object-style usage supported by cn to preserve all original classes and conditional logic.package.json (1)
66-66:@types/bunshould be indevDependencies.Type definition packages like
@types/bunare only needed during development/build time and should not be bundled with production dependencies.Proposed fix
Move
@types/bunfromdependenciestodevDependencies:"devDependencies": { "@biomejs/biome": "2.4.8", "@tailwindcss/postcss": "^4.2.1", "@tailwindcss/typography": "^0.5.19", + "@types/bun": "^1.3.11", "@types/node": "^25.4.0",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` at line 66, The dependency entry "@types/bun" currently listed under dependencies should be moved to devDependencies in package.json; update the package.json by removing the "@types/bun" entry from the dependencies object and adding the same version string to the devDependencies object, then reinstall/update lockfiles (npm/yarn) to reflect the change so production installs no longer include this dev-only type package.src/features/marketing/sections/use-cases.tsx (1)
134-136: Avoidsplit(" ")for mobile tab labels; it’s locale-fragile.Word-splitting assumes whitespace tokenization and can produce poor labels across localized content. Prefer explicit per-locale
shortLabelvalues.💡 Minimal refactor
const USE_CASES = [ { id: "disaster", label: "Disaster Response", + shortLabel: "Disaster", @@ - <span className="sm:hidden"> - {useCase.label.split(" ")[0]} - </span> + <span className="sm:hidden"> + {useCase.shortLabel ?? useCase.label} + </span>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/sections/use-cases.tsx` around lines 134 - 136, Replace the fragile split logic by using an explicit short label field for mobile: stop calling useCase.label.split(" ")[0] in the rendering and instead render useCase.shortLabel (fallback to useCase.label if shortLabel is missing); update the useCases data objects (where the array of useCase entries is defined) to include a per-locale shortLabel for each entry so mobile labels are stable and localization-safe (adjust the component that maps/renders use cases—the useCase variable in this file/component—so it reads shortLabel when present).src/utils/helpers.test.ts (1)
117-135: Environment cleanup may leave residual state if variables were previously undefined.
Object.assign(process.env, saved)restores values but doesn't re-delete keys that were removed during the test if they weren't originally present. Consider using a more robust cleanup pattern.♻️ Suggested safer cleanup pattern
test("returns empty string when no origin and no env vars", () => { const original = globalThis.location; try { // `@ts-expect-error` - testing without location delete globalThis.location; - const saved = { ...process.env }; + const savedVite = process.env.VITE_BASE_URL; + const savedNext = process.env.NEXT_PUBLIC_BASE_URL; + const savedBase = process.env.BASE_URL; delete process.env.VITE_BASE_URL; delete process.env.NEXT_PUBLIC_BASE_URL; delete process.env.BASE_URL; expect(getURL()).toBe(""); - Object.assign(process.env, saved); + if (savedVite !== undefined) process.env.VITE_BASE_URL = savedVite; + if (savedNext !== undefined) process.env.NEXT_PUBLIC_BASE_URL = savedNext; + if (savedBase !== undefined) process.env.BASE_URL = savedBase; } finally { if (original) { globalThis.location = original; } } });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/utils/helpers.test.ts` around lines 117 - 135, The test for getURL mutates process.env but only calls Object.assign(process.env, saved) which leaves keys created during the test if they weren't present originally; update the cleanup to restore environment exactly by capturing the saved env (const saved = { ...process.env }) then in the finally block first remove any keys that are present now but not in saved (iterate Object.keys(process.env) and delete keys not in saved), then Object.assign(process.env, saved); keep the existing globalThis.location restore logic as-is so location is reattached in finally.src/app/[locale]/not-found.tsx (1)
21-28: Consider adding NotFound translations to maintain consistency with the i18n infrastructure.The project uses
next-intlfor internationalization with message files for multiple locales (en, es, ar, zh, hi), but the messages do not currently include NotFound/404 keys. While this page displays hardcoded English text under the[locale]route, this pattern is consistent with other locale-specific pages in the codebase (e.g.,error.tsx,contact/page.tsx) that also use hardcoded English rather than translations. If translating error pages becomes a priority, you'll need to add NotFound keys to all message files and usegetTranslations()in the Server Component.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/not-found.tsx around lines 21 - 28, Add locale-aware messages for the NotFound page and replace the hardcoded English strings with translations: add NotFound.title and NotFound.description (or similarly named keys) to each locale messages file (en, es, ar, zh, hi), then in src/app/[locale]/not-found.tsx call next-intl's getTranslations (e.g., const t = getTranslations('NotFound')) in the server component and replace the h1 and paragraph content with t('title') and t('description') respectively so the page renders localized text for each locale.src/app/manifest.ts (2)
99-99: Inconsistent branding:short_nameis "MO".The
short_nameproperty is set to "MO" which appears to be leftover from previous branding. Consider updating to align with ResQ branding (e.g., "ResQ").🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/manifest.ts` at line 99, Update the app manifest's short_name value from the leftover "MO" to the ResQ branding (e.g., "ResQ") in the manifest object where short_name is defined so it matches other branding assets; locate the short_name entry in src/app/manifest.ts and replace "MO" with the desired short name (e.g., "ResQ") ensuring casing matches other identifiers.
1-2: License header should be at the top of the file.The Apache-2.0 license header is placed after the import statement. As per coding guidelines, the license header should be at the very beginning of the file.
♻️ Move license header to top
+/** + * Copyright 2025 ResQ Systems + * + * Licensed under the Apache License, Version 2.0 (the "License"); + ... + */ + import type { MetadataRoute } from 'next/types'; - -/** - * Copyright 2025 ResQ Systems - ...🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/manifest.ts` around lines 1 - 2, Move the Apache-2.0 license header to the very top of the file so it appears before any code or imports; specifically, place the license comment block above the existing import line (the import type { MetadataRoute } from 'next/types'; in manifest.ts) ensuring the license is the first content in the file.src/app/[locale]/(marketing)/protocol/page.tsx (2)
13-17: Metadata should also be localized.The
metadataexport uses hardcoded English strings. For proper SEO across all locales, consider usinggenerateMetadatawith locale-aware titles and descriptions.Example using generateMetadata
-export const metadata = constructMetadata({ - title: "Protocol", - description: - "The ResQ protocol is designed to operate when everything else has failed — a decentralized protocol built for zero-connectivity, GPS-denied, adversarial environments.", -}); +import { getTranslations } from "next-intl/server"; + +export async function generateMetadata({ params }: { params: { locale: string } }) { + const t = await getTranslations({ locale: params.locale, namespace: "Protocol" }); + return constructMetadata({ + title: t("label"), + description: t("description"), + }); +}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/protocol/page.tsx around lines 13 - 17, The current export const metadata = constructMetadata(...) uses hardcoded English strings; replace it with a locale-aware export async function generateMetadata({ params }) (or the Next.js generateMetadata signature used in this app) that returns the same constructMetadata(...) call but passes in title and description looked up from your localization resource (e.g., translations object or i18n/getTranslator) based on params.locale; update references to constructMetadata, metadata and page.tsx accordingly so metadata is generated per-locale instead of hardcoded.
23-36: Hero section text should use i18n translations.The hero section has hardcoded English text, but this page is within a locale-aware route (
[locale]). The text should use translations fromProtocol.*keys in the message catalogs for proper internationalization.Proposed fix using next-intl
+import { useTranslations } from "next-intl"; import { constructMetadata } from "@/utils/seo/meta"; // ... export default function ProtocolPage() { + const t = useTranslations("Protocol"); return ( <> {/* Inline hero header */} <section aria-labelledby="protocol-hero-heading" className="bg-bg-primary pt-32 pb-16 md:pt-40 md:pb-20"> <div className="container"> <p className="mb-6 font-mono text-xs uppercase tracking-[0.2em] text-accent-red"> - The Protocol + {t("label")} </p> <h1 id="protocol-hero-heading" className="max-w-4xl font-display text-4xl font-extrabold tracking-tight text-text-primary md:text-5xl lg:text-6xl"> - Built for Zero-Connectivity, GPS-Denied, Adversarial Environments + {t("heading")} </h1> <p className="mt-6 max-w-2xl text-lg text-text-secondary"> - The architecture assumption is inverted: ResQ is designed to operate - when everything else has failed, not when everything is working. + {t("description")} </p> </div> </section>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/protocol/page.tsx around lines 23 - 36, Replace the hardcoded hero texts with next-intl translations: import and call useTranslations('Protocol') (e.g., const t = useTranslations('Protocol')) in the page component, then replace the kicker "The Protocol", the h1 title "Built for Zero-Connectivity, GPS-Denied, Adversarial Environments" and the paragraph text with t('hero.kicker'), t('hero.title') and t('hero.description') (or equivalent Protocol.* keys from the message catalog), preserving the existing aria-labelledby/id and classNames so layout and accessibility remain unchanged.src/features/marketing/components/contact-form.tsx (2)
59-64: Consider extracting duplicated validation logic.The validation logic for each field is duplicated between
onBlurandonSubmithandlers. TanStack Form supports sharing validators to reduce duplication.Example refactor
+const nameValidator = ({ value }: { value: string }) => + !value ? "Name is required" : value.length > 200 ? "Name is too long" : undefined; + <form.Field name="name" validators={{ - onBlur: ({ value }) => - !value ? "Name is required" : value.length > 200 ? "Name is too long" : undefined, - onSubmit: ({ value }) => - !value ? "Name is required" : value.length > 200 ? "Name is too long" : undefined, + onBlur: nameValidator, + onSubmit: nameValidator, }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/contact-form.tsx` around lines 59 - 64, The validators for the Name field duplicate identical logic in the validators object (onBlur and onSubmit); extract a shared validator function (e.g., const validateName = ({ value }) => !value ? "Name is required" : value.length > 200 ? "Name is too long" : undefined) and replace both validators.onBlur and validators.onSubmit with references to that function so the same logic is reused; apply the same pattern for other fields if present and ensure the new function is imported/defined in the same scope as the contact form component.
34-45: Hardcoded UI strings should use i18n translations.The success message and all form labels/placeholders are hardcoded in English. Since the app supports 5 locales, these should use the translated strings from
Contact.form.*in the message catalogs.Example fix for success message
+"use client"; + +import { useTranslations } from "next-intl"; import { useForm } from "@tanstack/react-form"; // ... export function ContactForm() { + const t = useTranslations("Contact.form"); const [submitted, setSubmitted] = useState(false); // ... if (submitted) { return ( <div role="status" className="flex flex-col items-center justify-center gap-4 py-16 text-center"> <p className="font-mono text-xs font-bold uppercase tracking-widest text-accent-green"> - Transmission Received + {t("successTitle")} </p> <p className="max-w-sm text-sm text-text-secondary"> - We'll be in touch shortly. Thank you for reaching out. + {t("successBody")} </p> </div> ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/contact-form.tsx` around lines 34 - 45, The success UI block guarded by the `submitted` check currently uses hardcoded English strings; replace the static texts in that return (e.g., "Transmission Received" and "We'll be in touch shortly. Thank you for reaching out.") with i18n lookups using the project's message catalog keys (e.g., Contact.form.successTitle and Contact.form.successBody) via the existing translation hook/function used in this component (refer to where the component imports/uses the translator). Also update all other hardcoded labels/placeholders in this component (form labels, button text, placeholders) to use the corresponding Contact.form.* keys so the component supports all locales.messages/ar.json (1)
278-278: Consider using a localized email example for Arabic locale.The email placeholder uses
jane@example.comwhile the name placeholder correctly uses an Arabic name (محمد أحمد). For consistency, consider using an Arabic-region example likemohammed@example.comor similar.Suggested change
- "emailPlaceholder": "jane@example.com", + "emailPlaceholder": "mohammed@example.com",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@messages/ar.json` at line 278, Update the "emailPlaceholder" entry to use a localized Arabic-region example instead of "jane@example.com"; locate the "emailPlaceholder" key in the messages/ar.json translations and replace its value with an Arabic-friendly example such as "mohammed@example.com" (or another Arabic-script/localized address) to match the existing Arabic "namePlaceholder" localization.src/app/[locale]/layout.tsx (1)
97-97: Usecn()for the<html>class composition.
localeFontis optional here, and the repo standard is to route conditional Tailwind class composition throughsrc/lib/utils.♻️ Proposed refactor
+import { cn } from "@/lib/utils"; @@ - className={`dark ${syne.variable} ${dmSans.variable} ${dmMono.variable} ${localeFont}`} + className={cn( + "dark", + syne.variable, + dmSans.variable, + dmMono.variable, + localeFont, + )}As per coding guidelines, "Styling must use Tailwind CSS v4 with configuration in
src/styles/globals.css; usecn()utility fromsrc/lib/utilsfor conditional class composition".🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/layout.tsx at line 97, Replace the template literal className on the <html> element with the cn() utility and import it from src/lib/utils; specifically, add an import for cn and change className={`dark ${syne.variable} ${dmSans.variable} ${dmMono.variable} ${localeFont}`} to className={cn('dark', syne.variable, dmSans.variable, dmMono.variable, localeFont)} so the optional localeFont is handled correctly and class composition follows the repo standard.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2310f610-b5f8-4589-8416-a7304d85926c
⛔ Files ignored due to path filters (9)
bun.lockis excluded by!**/*.lockpublic/assets/images/active-fires.jpegis excluded by!**/*.jpegpublic/assets/images/car-crashes.jpegis excluded by!**/*.jpegpublic/assets/images/mass-casualty-incident.jpegis excluded by!**/*.jpegpublic/assets/images/natural-disasters.jpegis excluded by!**/*.jpegpublic/assets/images/swat-and-technical.jpegis excluded by!**/*.jpegpublic/assets/images/traffic-violation.jpegis excluded by!**/*.jpegpublic/assets/videos/Futuristic_Drone_Lands_On_Logo.mp4is excluded by!**/*.mp4public/assets/videos/drone-landing-video.mp4is excluded by!**/*.mp4
📒 Files selected for processing (81)
.gitignorecontent/posts/building-for-zero-connectivity.mdxcontent/posts/decentralized-mesh-networks-save-lives.mdxcontent/posts/introducing-resq-tactical-os.mdxenv.d.tsinstrumentation-client.tsmessages/ar.jsonmessages/en.jsonmessages/es.jsonmessages/hi.jsonmessages/zh.jsonnext.config.tspackage.jsonpublic/assets/images/active-fires.webppublic/assets/images/car-crashes.webppublic/assets/images/mass-casualty-incident.webppublic/assets/images/natural-disasters.webppublic/assets/images/swat-and-technical.webppublic/assets/images/traffic-violation.webppublic/sitemap.xmlsrc/actions/contact/submit-contact.tssrc/actions/contact/submit.tssrc/app/(marketing)/layout.tsxsrc/app/[locale]/(marketing)/blog/[slug]/page.tsxsrc/app/[locale]/(marketing)/blog/page.tsxsrc/app/[locale]/(marketing)/contact/page.tsxsrc/app/[locale]/(marketing)/layout.tsxsrc/app/[locale]/(marketing)/legal/layout.tsxsrc/app/[locale]/(marketing)/legal/privacy/page.tsxsrc/app/[locale]/(marketing)/legal/terms/page.tsxsrc/app/[locale]/(marketing)/page.tsxsrc/app/[locale]/(marketing)/platform/page.tsxsrc/app/[locale]/(marketing)/protocol/page.tsxsrc/app/[locale]/error.tsxsrc/app/[locale]/forbidden.tsxsrc/app/[locale]/layout.tsxsrc/app/[locale]/loading.tsxsrc/app/[locale]/not-found.tsxsrc/app/api/[[...route]]/health/handlers.tssrc/app/global-error.tsxsrc/app/layout.tsxsrc/app/manifest.tssrc/app/sitemap.tssrc/config/config.tssrc/constants/app.tssrc/env.tssrc/features/marketing/components/contact-form.tsxsrc/features/marketing/components/footer.tsxsrc/features/marketing/components/hash-scroll-center.tsxsrc/features/marketing/components/hero-hud.tsxsrc/features/marketing/components/hero-video-slideshow.tsxsrc/features/marketing/components/navbar.tsxsrc/features/marketing/sections/architecture-diagram.tsxsrc/features/marketing/sections/competitive-table.tsxsrc/features/marketing/sections/footer-cta.tsxsrc/features/marketing/sections/hero.tsxsrc/features/marketing/sections/how-it-works.tsxsrc/features/marketing/sections/index.tssrc/features/marketing/sections/metrics-strip.tsxsrc/features/marketing/sections/platform-pillars.tsxsrc/features/marketing/sections/problem-statement.tsxsrc/features/marketing/sections/technology.tsxsrc/features/marketing/sections/trust-security.tsxsrc/features/marketing/sections/use-cases.tsxsrc/features/marketing/sections/why-now.tsxsrc/hooks/index.tssrc/i18n/navigation.tssrc/i18n/request.tssrc/i18n/routing.tssrc/lib/content.tssrc/lib/utils.test.tssrc/mdx-components.tsxsrc/proxy.tssrc/styles/globals.csssrc/utils/helpers.test.tssrc/utils/helpers.tssrc/utils/logger.test.tssrc/utils/seo/meta.test.tssrc/utils/seo/meta.tssrc/utils/seo/speculation-schema.test.tstest.ts
💤 Files with no reviewable changes (4)
- src/app/[locale]/forbidden.tsx
- src/app/(marketing)/layout.tsx
- public/sitemap.xml
- src/actions/contact/submit-contact.ts
| }: { params: Promise<{ slug: string }> }): Promise<Metadata> { | ||
| return params.then(({ slug }) => { | ||
| const post = getPostBySlug(slug); | ||
| if (!post) return constructMetadata({ title: "Post Not Found" }); |
There was a problem hiding this comment.
This route is still pinned to English.
"Post Not Found", "Back to blog", and toLocaleDateString("en-US", …) all bypass the active locale, so non-English routes will still render mixed-language metadata and page chrome. Please source these strings from translations and format the date with the current locale.
Also applies to: 31-36, 52-57
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx at line 22, The page is
using hard-coded English strings and a fixed date locale; replace those with
localized translations and date formatting using the active locale: use your
existing i18n/dictionary lookup (e.g., getDictionary or t) to source the "Post
Not Found" and "Back to blog" strings instead of the literals used around
constructMetadata and in the page markup, and replace
toLocaleDateString("en-US", …) calls with Intl.DateTimeFormat(locale,
options).format(date) (where locale comes from the route/params or your locale
provider) so the metadata and page chrome (the constructMetadata call, post
title/summary rendering, and the back link) all respect the current locale.
| const TRUST_CARDS: ReadonlyArray<{ | ||
| icon: ComponentType<{ className?: string; strokeWidth?: number }>; | ||
| title: string; | ||
| description: string; | ||
| }> = [ | ||
| { | ||
| icon: Lock, | ||
| title: "AES-256 Encryption", | ||
| description: | ||
| "Military-grade encryption for all mesh communications. End-to-end by default, no exceptions.", | ||
| }, | ||
| { | ||
| icon: Globe, | ||
| title: "Decentralized Architecture", | ||
| description: | ||
| "No central server to compromise. Every node is sovereign. The network survives any single point of failure.", | ||
| }, | ||
| { | ||
| icon: Shield, | ||
| title: "Blockchain-Verified GPS", | ||
| description: | ||
| "Tamper-proof positioning backed by distributed ledger consensus. No spoofing, no drift.", | ||
| }, | ||
| { | ||
| icon: Server, | ||
| title: "Edge-First Processing", | ||
| description: | ||
| "All computation happens on-device at the edge. Your data never leaves the mesh unless you authorize it.", | ||
| }, | ||
| ]; | ||
|
|
||
| const COMPLIANCE_BADGES = [ | ||
| "FIPS 140-2", | ||
| "SOC 2 Type II", | ||
| "CJIS Compliant", | ||
| "US Data Residency", | ||
| ] as const; |
There was a problem hiding this comment.
Move Trust & Security copy to locale-aware content sources.
This section hardcodes all user-facing strings in English (cards, heading, badges), which breaks the multi-locale objective for localized marketing pages.
💡 Suggested direction (localized content injection)
-const TRUST_CARDS = [
+type TrustCard = Readonly<{
+ icon: ComponentType<{ className?: string; strokeWidth?: number }>;
+ title: string;
+ description: string;
+}>;
-const COMPLIANCE_BADGES = [
- "FIPS 140-2",
- "SOC 2 Type II",
- "CJIS Compliant",
- "US Data Residency",
-] as const;
-
-export function TrustSecurity() {
+export function TrustSecurity({
+ sectionLabel,
+ heading,
+ cards,
+ badges,
+}: Readonly<{
+ sectionLabel: string;
+ heading: string;
+ cards: ReadonlyArray<TrustCard>;
+ badges: ReadonlyArray<string>;
+}>) {
@@
- <p className="mb-3 md:mb-4 font-mono text-xs uppercase tracking-[0.2em] text-accent-red">
- 08 {"//"} Trust & Security
- </p>
+ <p className="mb-3 md:mb-4 font-mono text-xs uppercase tracking-[0.2em] text-accent-red">
+ {sectionLabel}
+ </p>
@@
- Built for Zero-Trust Environments
+ {heading}
@@
- {TRUST_CARDS.map((card, i) => {
+ {cards.map((card, i) => {
@@
- {COMPLIANCE_BADGES.map((badge, i) => (
+ {badges.map((badge, i) => (Also applies to: 68-73, 95-100, 115-121
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/sections/trust-security.tsx` around lines 10 - 46, The
hardcoded English strings in TRUST_CARDS and COMPLIANCE_BADGES must be moved to
the app's localization layer and replaced with locale-aware lookups; update the
trust-security.tsx file to construct TRUST_CARDS and COMPLIANCE_BADGES using
your i18n/getText function (e.g., call t('trust.cards[0].title'),
t('trust.cards[0].description'), t('trust.badges[0]') or load a locale-aware
trustContent object) instead of literal strings, keep the same shapes/types
(icon: Lock/Globe/Shield/Server, title, description) so existing rendering code
that references TRUST_CARDS and COMPLIANCE_BADGES continues to work, and apply
the same change for the other hardcoded copies noted (the blocks around lines
referenced in the comment) so all user-facing copy is sourced from translations
or locale content providers.
src/utils/helpers.ts
Outdated
| export const ScrollIntoCenterView = (href: string): boolean => { | ||
| if (typeof document === "undefined") { | ||
| return false; | ||
| } | ||
|
|
||
| return scrollElementIntoCenterView(document.querySelector(href)); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, check the file at the specified lines
cat -n src/utils/helpers.ts | sed -n '150,165p'Repository: resq-software/landing
Length of output: 415
🏁 Script executed:
# Find the scrollElementIntoCenterView function to understand what it does
rg -n "scrollElementIntoCenterView" --type ts --type tsx -B 2 -A 10Repository: resq-software/landing
Length of output: 92
🏁 Script executed:
# Get overall context of the helpers.ts file
wc -l src/utils/helpers.tsRepository: resq-software/landing
Length of output: 89
🏁 Script executed:
# Search for scrollElementIntoCenterView without file type restrictions
rg -n "scrollElementIntoCenterView" -B 2 -A 10Repository: resq-software/landing
Length of output: 1290
🏁 Script executed:
# Also check the whole helpers.ts file to understand the pattern
head -n 160 src/utils/helpers.tsRepository: resq-software/landing
Length of output: 4992
🏁 Script executed:
# Search for usages of ScrollIntoCenterView function
rg -n "ScrollIntoCenterView" -B 2 -A 3Repository: resq-software/landing
Length of output: 346
🏁 Script executed:
# Check if there are tests that show expected usage
fd -e test.ts -e spec.ts -e test.tsx -e spec.tsx | head -20Repository: resq-software/landing
Length of output: 205
🏁 Script executed:
# Check the test file for ScrollIntoCenterView
cat src/utils/helpers.test.tsRepository: resq-software/landing
Length of output: 4707
Add error handling to prevent unhandled exceptions from document.querySelector().
document.querySelector() throws a DOMException when passed an invalid CSS selector. Since the function signature guarantees a boolean return type, invalid href values will break the function's contract. Wrap the querySelector call in a try-catch block to safely return false for any selector errors.
Proposed fix
export const ScrollIntoCenterView = (href: string): boolean => {
if (typeof document === "undefined") {
return false;
}
- return scrollElementIntoCenterView(document.querySelector(href));
+ try {
+ return scrollElementIntoCenterView(document.querySelector(href));
+ } catch {
+ logger.warn(`ScrollIntoCenterView: invalid selector "${href}"`);
+ return false;
+ }
};📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const ScrollIntoCenterView = (href: string): boolean => { | |
| if (typeof document === "undefined") { | |
| return false; | |
| } | |
| return scrollElementIntoCenterView(document.querySelector(href)); | |
| export const ScrollIntoCenterView = (href: string): boolean => { | |
| if (typeof document === "undefined") { | |
| return false; | |
| } | |
| try { | |
| return scrollElementIntoCenterView(document.querySelector(href)); | |
| } catch { | |
| logger.warn(`ScrollIntoCenterView: invalid selector "${href}"`); | |
| return false; | |
| } | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/helpers.ts` around lines 154 - 159, ScrollIntoCenterView can throw
if document.querySelector receives an invalid selector; wrap the querySelector
call in a try-catch inside ScrollIntoCenterView and return false on any
exception, and also return false if querySelector returns null before calling
scrollElementIntoCenterView; reference the ScrollIntoCenterView function and the
scrollElementIntoCenterView call when applying this change.
| test("defaults to ALL level in non-production", () => { | ||
| const original = process.env.NODE_ENV; | ||
| process.env.NODE_ENV = "development"; | ||
|
|
||
| const logger = new Logger("TestDev"); | ||
| logger.trace("trace msg"); | ||
| logger.debug("debug msg"); | ||
| logger.info("info msg"); | ||
|
|
||
| process.env.NODE_ENV = original; | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd /tmp && find . -name "logger.test.ts" 2>/dev/null | head -5Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
git ls-files | grep -i logger.testRepository: resq-software/landing
Length of output: 89
🏁 Script executed:
find . -type f -name "*.test.ts" | head -10Repository: resq-software/landing
Length of output: 215
🏁 Script executed:
git ls-files | grep -E "src/.*logger" | head -20Repository: resq-software/landing
Length of output: 109
🏁 Script executed:
cat -n src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 8557
Use finally blocks or test lifecycle hooks to restore global state; await async rejection assertions.
Tests outside lines 54–132 restore process.env and console only on the success path—if a test throws, later tests inherit the modified state. Additionally, at lines 219–223, the rejection assertion lacks await or return, causing the test to pass without verifying the Promise rejection.
Move restoration into finally blocks or adopt beforeEach/afterEach hooks (as done in lines 54–85). For process.env.NODE_ENV, use delete process.env.NODE_ENV if the original value was undefined. For the async assertion, add await or return before expect().
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/logger.test.ts` around lines 15 - 25, Several tests (including the
one instantiating Logger("TestDev")) change process.env.NODE_ENV and console
state but only restore them on the success path; move restoration into finally
blocks or use test lifecycle hooks (beforeEach/afterEach) to ensure process.env
and console are restored even on failures, and when restoring NODE_ENV if the
saved original is undefined use delete process.env.NODE_ENV instead of assigning
undefined. Also update the async rejection assertion (the expect(...).rejects
usage around the promise at lines ~219–223) to use await or return (e.g., await
expect(...).rejects...) so the test actually waits for and verifies the Promise
rejection.
| expect( | ||
| logger.time("failing", () => { | ||
| throw new Error("boom"); | ||
| }), | ||
| ).rejects.toThrow("boom"); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd "$(git rev-parse --show-toplevel)" && pwdRepository: resq-software/landing
Length of output: 83
🏁 Script executed:
git ls-files "src/utils/logger*" | head -20Repository: resq-software/landing
Length of output: 109
🏁 Script executed:
wc -l src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 93
🏁 Script executed:
sed -n '200,230p' src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 809
🏁 Script executed:
sed -n '1,50p' src/utils/logger.tsRepository: resq-software/landing
Length of output: 1654
🏁 Script executed:
grep -n "time(" src/utils/logger.ts | head -5Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
sed -n '100,180p' src/utils/logger.tsRepository: resq-software/landing
Length of output: 2735
🏁 Script executed:
grep -A 20 "^\s*time(" src/utils/logger.tsRepository: resq-software/landing
Length of output: 47
🏁 Script executed:
rg "^\s*time\(" src/utils/logger.ts -A 20Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
rg "time" src/utils/logger.ts -B 2 -A 10Repository: resq-software/landing
Length of output: 1538
🏁 Script executed:
wc -l src/utils/logger.tsRepository: resq-software/landing
Length of output: 88
🏁 Script executed:
sed -n '200,250p' src/utils/logger.tsRepository: resq-software/landing
Length of output: 1522
🏁 Script executed:
sed -n '210,230p' src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 481
🏁 Script executed:
sed -n '205,230p' src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 669
🏁 Script executed:
ast-grep --pattern 'test($_, async () => { $$$ })' src/utils/logger.test.tsRepository: resq-software/landing
Length of output: 1552
🏁 Script executed:
sed -n '250,280p' src/utils/logger.tsRepository: resq-software/landing
Length of output: 1007
🏁 Script executed:
sed -n '245,265p' src/utils/logger.tsRepository: resq-software/landing
Length of output: 756
🏁 Script executed:
rg "async time" src/utils/logger.ts -A 15Repository: resq-software/landing
Length of output: 682
Await the .rejects assertion.
The logger.time() method returns a Promise. Without await or return, the async assertion chain is not waited on, and the test body completes without validating the rejection. This creates a false positive—the test passes even if the assertion fails.
✅ Minimal fix
- expect(
+ await expect(
logger.time("failing", () => {
throw new Error("boom");
}),
).rejects.toThrow("boom");📝 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.
| expect( | |
| logger.time("failing", () => { | |
| throw new Error("boom"); | |
| }), | |
| ).rejects.toThrow("boom"); | |
| await expect( | |
| logger.time("failing", () => { | |
| throw new Error("boom"); | |
| }), | |
| ).rejects.toThrow("boom"); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/logger.test.ts` around lines 219 - 223, The test uses
expect(...).rejects.toThrow on the Promise returned by logger.time("failing",
...) but does not await or return that expectation, so the test can falsely
pass; update the test to await the assertion (or return the expect promise) so
the rejection is actually asserted — locate the failing test that calls
logger.time("failing", () => { throw new Error("boom"); }) and add an await
before expect(...) or return the expect(...) to ensure the promise rejection is
validated.
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/app/sitemap.ts (1)
1-6:⚠️ Potential issue | 🟡 MinorMissing Apache-2.0 license header.
The file is missing the required license header at the top.
Proposed fix to add the license header
+/** + * Copyright 2026 ResQ Software + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + /** * Generates the static sitemap metadata for the marketing routes. */As per coding guidelines: "All source files must include the Apache-2.0 license header".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/sitemap.ts` around lines 1 - 6, Add the required Apache-2.0 license header to the top of src/app/sitemap.ts so the file includes the standard license block (SPDX identifier and copyright/owner information) before any imports; update the header consistently with other project files to match formatting and owner/year, ensuring the licence appears above the existing comments and the import of MetadataRoute and config.
♻️ Duplicate comments (1)
src/app/[locale]/loading.tsx (1)
43-48:⚠️ Potential issue | 🔴 CriticalDebug timeout value left in code — 30-second delay will block users.
The
setTimeoutdelay is set to30_000ms (30 seconds) with a TODO comment indicating it should be1200. This appears to be debug/testing code that was inadvertently committed and will cause an unacceptable delay before the loading screen dismisses.Additionally, the
setTimeoutreturn value is not captured and cleared in the effect cleanup, which can cause state updates after unmount.Suggested fix
useEffect(() => { let currentIndex = 0; + let hideTimeout: ReturnType<typeof setTimeout> | undefined; document.body.style.overflow = 'hidden'; document.body.setAttribute('aria-busy', 'true'); const interval = setInterval(() => { if (currentIndex < bootSequence.length) { setLogs(prev => [...prev, bootSequence[currentIndex]]); setProgress(((currentIndex + 1) / bootSequence.length) * 100); currentIndex++; } else { clearInterval(interval); - setTimeout(() => { + hideTimeout = setTimeout(() => { setLoading(false); document.body.style.overflow = ''; document.body.removeAttribute('aria-busy'); - }, 30_000); // TODO: revert to 1200 after design review + }, 1200); } }, 450); return () => { clearInterval(interval); + if (hideTimeout) clearTimeout(hideTimeout); document.body.style.overflow = ''; document.body.removeAttribute('aria-busy'); }; }, []);,
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/loading.tsx around lines 43 - 48, Replace the debug 30_000ms delay with the intended 1200ms and ensure the timer is cleaned up: in the effect that calls setTimeout (the block that calls setLoading(false), document.body.style.overflow = '', and document.body.removeAttribute('aria-busy')), store the setTimeout return value in a variable (e.g., timerId) and call clearTimeout(timerId) in the effect/component cleanup to avoid state updates after unmount; keep the same state update (setLoading) and DOM cleanup calls but ensure the timer is cleared on unmount.
🧹 Nitpick comments (3)
src/features/marketing/sections/competitive-table.tsx (1)
19-26: Consider adding explicit TypeScript types for better maintainability.The
COMPETITORSarray uses mixedboolean | stringvalues inchecks, but there's no type definition ensuring each entry has exactly 6 checks matching the 6 features. Adding explicit types would catch mismatches at compile time.♻️ Suggested type definition
+type CheckValue = boolean | "partial"; + +interface Competitor { + name: string; + checks: [CheckValue, CheckValue, CheckValue, CheckValue, CheckValue, CheckValue]; + highlight?: boolean; + note: string; +} + -const COMPETITORS = [ +const COMPETITORS: Competitor[] = [ { name: "ResQ", checks: [true, true, true, true, true, true], highlight: true, note: "OS-level coordination layer" },This ensures
checkslength matchesFEATURESlength and restricts values to valid types.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/sections/competitive-table.tsx` around lines 19 - 26, The COMPETITORS constant uses a mixed boolean|string checks array with no type safety; add an explicit TypeScript type (e.g., a Competitor interface or type alias) that defines name:string, note?:string, highlight?:boolean and checks as a fixed-length tuple of exactly six elements with allowed values boolean | "partial" (or a union type like Check = true | false | "partial") and then annotate COMPETITORS: Competitor[] (or readonly Competitor[]) to ensure compile-time validation that each entry has exactly six checks matching FEATURES; update any usages expecting FEATURES to use the same length/type to keep them in sync.src/features/marketing/components/footer.tsx (1)
52-61: Missing error handling for status fetch failure.The
catch(() => {})silently swallows errors. While the UI gracefully falls back to "unknown" status, consider logging the error for debugging purposes.🔧 Suggested improvement
.then((json) => { if (json.success && json.data) { setStatus(json.data); } }) - .catch(() => {}); + .catch((err) => { + console.error("Failed to fetch status:", err); + });🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/footer.tsx` around lines 52 - 61, The current useEffect that calls fetch("/api/status") swallows errors in catch(() => {})—replace the empty catch with proper error logging so failures are visible; update the catch to log the caught error (and optionally any non-OK response details) using console.error (or the app logger) and retain the existing fallback behavior via setStatus, referencing the useEffect block, the fetch("/api/status") call, and the setStatus state updater in the Footer component.src/features/marketing/components/locale-switcher.tsx (1)
78-109: Consider adding keyboard navigation for the dropdown.The compact variant dropdown has good ARIA attributes (
role="listbox",role="option",aria-selected), but lacks keyboard navigation. Users cannot use arrow keys to navigate options, which is expected behavior for listbox patterns per WCAG.This is a minor accessibility enhancement that could be deferred.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/locale-switcher.tsx` around lines 78 - 109, The dropdown lacks keyboard navigation for the listbox pattern; add a keydown handler on the listbox container (the element with role="listbox" in locale-switcher.tsx) that tracks a focused option index (local state) and responds to ArrowDown/ArrowUp/Home/End to move the index, and Enter/Space to activate switchLocale(loc) for the focused item; ensure the handler calls setOpen(false) when activating, updates aria-selected appropriately (compare routing.locales[index] to locale), and moves DOM focus to the corresponding option element (or use aria-activedescendant) so keyboard users can navigate the LOCALE_LABELS options and select one without a mouse.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/features/marketing/components/footer.tsx`:
- Line 186: The X SVG in the Footer component lacks an aria-labelledby
connection to its <title>, so update the SVG element used in footer.tsx (the X
icon) to include an aria-labelledby attribute pointing to a unique id and give
the <title> element that same id (e.g., "x-title-<unique>"). Ensure the title
text is descriptive (e.g., "X") and keep role="img"; use a unique id to avoid
collisions if the icon is rendered multiple times.
- Line 195: The GitHub SVG in the Footer component lacks an aria-labelledby
association for its <title>, so add a unique id to the <title> element (e.g.,
"github-title") and add aria-labelledby="github-title" on the <svg> element (the
SVG block containing <title>GitHub</title>), ensuring the title is properly
associated for screen readers; keep the existing role="img" and
className="size-5".
- Line 177: The SVG in the footer component has the <title> element placed after
the <path>, causing the Biome noSvgWithoutTitle lint error; move the <title>
element to be the first child of the <svg> with className "size-5" (the LinkedIn
icon SVG) so <title> appears before the <path> inside the SVG element in the
Footer component.
---
Outside diff comments:
In `@src/app/sitemap.ts`:
- Around line 1-6: Add the required Apache-2.0 license header to the top of
src/app/sitemap.ts so the file includes the standard license block (SPDX
identifier and copyright/owner information) before any imports; update the
header consistently with other project files to match formatting and owner/year,
ensuring the licence appears above the existing comments and the import of
MetadataRoute and config.
---
Duplicate comments:
In `@src/app/`[locale]/loading.tsx:
- Around line 43-48: Replace the debug 30_000ms delay with the intended 1200ms
and ensure the timer is cleaned up: in the effect that calls setTimeout (the
block that calls setLoading(false), document.body.style.overflow = '', and
document.body.removeAttribute('aria-busy')), store the setTimeout return value
in a variable (e.g., timerId) and call clearTimeout(timerId) in the
effect/component cleanup to avoid state updates after unmount; keep the same
state update (setLoading) and DOM cleanup calls but ensure the timer is cleared
on unmount.
---
Nitpick comments:
In `@src/features/marketing/components/footer.tsx`:
- Around line 52-61: The current useEffect that calls fetch("/api/status")
swallows errors in catch(() => {})—replace the empty catch with proper error
logging so failures are visible; update the catch to log the caught error (and
optionally any non-OK response details) using console.error (or the app logger)
and retain the existing fallback behavior via setStatus, referencing the
useEffect block, the fetch("/api/status") call, and the setStatus state updater
in the Footer component.
In `@src/features/marketing/components/locale-switcher.tsx`:
- Around line 78-109: The dropdown lacks keyboard navigation for the listbox
pattern; add a keydown handler on the listbox container (the element with
role="listbox" in locale-switcher.tsx) that tracks a focused option index (local
state) and responds to ArrowDown/ArrowUp/Home/End to move the index, and
Enter/Space to activate switchLocale(loc) for the focused item; ensure the
handler calls setOpen(false) when activating, updates aria-selected
appropriately (compare routing.locales[index] to locale), and moves DOM focus to
the corresponding option element (or use aria-activedescendant) so keyboard
users can navigate the LOCALE_LABELS options and select one without a mouse.
In `@src/features/marketing/sections/competitive-table.tsx`:
- Around line 19-26: The COMPETITORS constant uses a mixed boolean|string checks
array with no type safety; add an explicit TypeScript type (e.g., a Competitor
interface or type alias) that defines name:string, note?:string,
highlight?:boolean and checks as a fixed-length tuple of exactly six elements
with allowed values boolean | "partial" (or a union type like Check = true |
false | "partial") and then annotate COMPETITORS: Competitor[] (or readonly
Competitor[]) to ensure compile-time validation that each entry has exactly six
checks matching FEATURES; update any usages expecting FEATURES to use the same
length/type to keep them in sync.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 96044511-0d01-45c4-9773-a9e12a113833
📒 Files selected for processing (20)
messages/ar.jsonmessages/en.jsonmessages/es.jsonmessages/hi.jsonmessages/zh.jsonsrc/app/[locale]/(marketing)/legal/acceptable-use/page.tsxsrc/app/[locale]/(marketing)/legal/cookie-policy/page.tsxsrc/app/[locale]/(marketing)/legal/data-processing/page.tsxsrc/app/[locale]/(marketing)/legal/page.tsxsrc/app/[locale]/(marketing)/legal/security/page.tsxsrc/app/[locale]/loading.tsxsrc/app/sitemap.tssrc/features/marketing/components/footer.tsxsrc/features/marketing/components/locale-switcher.tsxsrc/features/marketing/components/navbar.tsxsrc/features/marketing/sections/architecture-diagram.tsxsrc/features/marketing/sections/competitive-table.tsxsrc/features/marketing/sections/technology.tsxsrc/features/marketing/sections/trust-security.tsxsrc/features/marketing/sections/why-now.tsx
✅ Files skipped from review due to trivial changes (6)
- messages/hi.json
- messages/es.json
- messages/zh.json
- src/features/marketing/sections/trust-security.tsx
- messages/en.json
- messages/ar.json
🚧 Files skipped from review as they are similar to previous changes (3)
- src/features/marketing/sections/architecture-diagram.tsx
- src/features/marketing/sections/technology.tsx
- src/features/marketing/sections/why-now.tsx
…n issues - Guard Logtail init for missing BETTERSTACK_API_KEY (fixes CI build) - Update Biome schema 2.4.7→2.4.8 and fix useLiteralKeys lint errors - Add aria-labelledby to footer SVGs (fixes noSvgWithoutTitle errors) - Pin CI test job actions to SHA hashes for supply chain safety - Remove PII logging from contact form action (GDPR/CCPA) - Clear setTimeout in loading.tsx useEffect cleanup (memory leak) - Add Apache-2.0 license header to loading.tsx - Use locale-aware Link and date formatting in blog pages - Generate locale-prefixed sitemap URLs for all 5 locales - Fix locale-blind MDX links via relative paths - Validate frontmatter in content loader instead of unsafe casts - Remove dead ScrollIntoCenterView helper and ad-hoc test.ts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
src/app/[locale]/(marketing)/blog/[slug]/page.tsx (1)
18-23:⚠️ Potential issue | 🟠 MajorLocalize metadata fallback and back-link label.
Line 23 (
"Post Not Found") and Line 58 ("Back to blog") are still hard-coded English, so non-English locales render mixed-language metadata/chrome. Use the active locale dictionary for both strings.Also applies to: 53-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx around lines 18 - 23, The metadata title and the page back-link are hard-coded in English; update generateMetadata and the page render to use the active locale dictionary instead: extract the locale from the route params, call the existing getDictionary (or equivalent) for that locale, and replace the hard-coded "Post Not Found" passed into constructMetadata and the hard-coded "Back to blog" link label (lines ~53-59) with the appropriate localized keys (e.g., dict.postNotFound and dict.backToBlog) so both the metadata title and back-link label are rendered in the current locale.
🧹 Nitpick comments (3)
src/features/marketing/components/footer.tsx (2)
52-61: Empty catch block silently swallows fetch errors.The error is discarded without logging, making debugging harder if the status endpoint fails. Consider at least logging to console in development.
🔧 Suggested improvement
useEffect(() => { fetch("/api/status") .then((res) => res.json()) .then((json) => { if (json.success && json.data) { setStatus(json.data); } }) - .catch(() => {}); + .catch((error) => { + if (process.env.NODE_ENV === "development") { + console.error("Failed to fetch status:", error); + } + }); }, []);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/footer.tsx` around lines 52 - 61, The empty catch in the useEffect that calls fetch("/api/status") silently swallows errors; update the catch on the fetch chain in the component (the useEffect that calls fetch("/api/status") and then setStatus) to at minimum log the error (e.g., console.error) and include a descriptive message, and preferably gate verbose logging behind a dev check (process.env.NODE_ENV !== 'production') or forward the error to an existing logger if available.
139-155: Newsletter form is non-functional.The form prevents default submission but doesn't actually do anything. Consider adding a TODO comment to track this or implementing basic user feedback (e.g., a toast indicating the feature is coming soon).
🔧 Suggested placeholder feedback
<form className="flex w-full max-w-md" - onSubmit={(e) => e.preventDefault()} + onSubmit={(e) => { + e.preventDefault(); + // TODO: Implement newsletter subscription + }} >🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/components/footer.tsx` around lines 139 - 155, The newsletter form currently stops submission in the inline onSubmit handler but does nothing else; add a minimal placeholder implementation by extracting the submit logic into a handler (e.g., handleNewsletterSubmit) and either (a) add a TODO comment referencing handleNewsletterSubmit to track future work, or (b) implement immediate user feedback in that handler (e.g., trigger a toast/alert or set local state like isSubscribed/message to show a "coming soon" or "subscription received" message), and wire that handler to the form's onSubmit (and ensure the submit button remains type="submit"); update the input to be controlled or read its value in handleNewsletterSubmit if you plan to show it in the message.src/lib/better-stack.ts (1)
27-33: Consider extracting aLoggerLiketype instead of force-casting toLogtailLine 32 force-casts a partial stub to
Logtail. While the stub correctly implements all methods used within this file (debug,info,warn,error,flush) and the logger is wrapped by typed helper functions that provide type safety at the API boundary, extracting an explicit interface would improve maintainability and prevent future accidental unsafe casts.Suggested refactor
import { env } from '@/env'; import { Logtail } from '@logtail/node'; +type LoggerLike = { + debug: (...args: any[]) => Promise<unknown>; + info: (...args: any[]) => Promise<unknown>; + warn: (...args: any[]) => Promise<unknown>; + error: (...args: any[]) => Promise<unknown>; + flush: () => Promise<void>; +}; + -function createLogger(): Logtail { +function createLogger(): LoggerLike { const token = env.BETTERSTACK_API_KEY; if (!token) { - const noop = (..._args: unknown[]) => Promise.resolve({} as never); - return { debug: noop, info: noop, warn: noop, error: noop, flush: () => Promise.resolve() } as unknown as Logtail; + const noop = async (..._args: any[]) => undefined; + return { + debug: noop, + info: noop, + warn: noop, + error: noop, + flush: async () => undefined, + }; } return new Logtail(token, {🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/better-stack.ts` around lines 27 - 33, The stub in createLogger is being force-cast to Logtail; instead extract a LoggerLike interface describing the used methods (debug, info, warn, error, flush), type the noop stub as LoggerLike, and change createLogger's return type from Logtail to LoggerLike (or union Logtail|LoggerLike) so callers use the explicit lightweight logger interface; update any call sites that assume Logtail accordingly (refer to createLogger, noop, and Logtail).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx:
- Around line 32-37: The formatDate function currently uses new Date(dateStr)
which treats a date-only string as UTC midnight and then toLocaleDateString can
shift it to the previous day in negative-offset timezones; update formatDate to
parse the YYYY-MM-DD string into numeric year, month, and day (e.g., split on
"-" and convert to numbers), construct a Date using Date.UTC(year, month-1, day)
or new Date(Date.UTC(...)), and call toLocaleDateString with an explicit
timeZone: "UTC" option so the displayed date does not drift across timezones.
In `@src/app/`[locale]/loading.tsx:
- Around line 47-51: The timeout for hiding the loading state in the hideTimeout
callback is set to 30_000ms (30s) but the TODO says it should be 1_200ms; change
the delay passed to setTimeout in the hideTimeout setup to 1_200 (or wrap the
30_000 value behind a development-only guard like process.env.NODE_ENV !==
'production' so production uses 1_200ms) and ensure the rest of the callback
still calls setLoading(false), resets document.body.style.overflow, and removes
the aria-busy attribute as implemented in this component (the
hideTimeout/setLoading/document.body.* logic).
- Around line 37-38: The loading component in loading.tsx and the navbar in
navbar.tsx both directly set document.body.style.overflow, causing a race when
the loading timeout clears overflow while the mobile menu is open; replace
direct DOM writes with a shared scroll-lock manager that provides
reference-counted lock/unlock functions (e.g.,
acquireScrollLock/releaseScrollLock or lock/unlock) exported from a new module
and call those from the Loading component (on mount/timeout/cleanup) and from
the Navbar (when opening/closing the mobile menu) so each consumer
increments/decrements a counter and only clears overflow when the count reaches
zero; update Loading and Navbar to use these manager functions instead of
directly setting document.body.style.overflow or aria attributes.
---
Duplicate comments:
In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx:
- Around line 18-23: The metadata title and the page back-link are hard-coded in
English; update generateMetadata and the page render to use the active locale
dictionary instead: extract the locale from the route params, call the existing
getDictionary (or equivalent) for that locale, and replace the hard-coded "Post
Not Found" passed into constructMetadata and the hard-coded "Back to blog" link
label (lines ~53-59) with the appropriate localized keys (e.g.,
dict.postNotFound and dict.backToBlog) so both the metadata title and back-link
label are rendered in the current locale.
---
Nitpick comments:
In `@src/features/marketing/components/footer.tsx`:
- Around line 52-61: The empty catch in the useEffect that calls
fetch("/api/status") silently swallows errors; update the catch on the fetch
chain in the component (the useEffect that calls fetch("/api/status") and then
setStatus) to at minimum log the error (e.g., console.error) and include a
descriptive message, and preferably gate verbose logging behind a dev check
(process.env.NODE_ENV !== 'production') or forward the error to an existing
logger if available.
- Around line 139-155: The newsletter form currently stops submission in the
inline onSubmit handler but does nothing else; add a minimal placeholder
implementation by extracting the submit logic into a handler (e.g.,
handleNewsletterSubmit) and either (a) add a TODO comment referencing
handleNewsletterSubmit to track future work, or (b) implement immediate user
feedback in that handler (e.g., trigger a toast/alert or set local state like
isSubscribed/message to show a "coming soon" or "subscription received"
message), and wire that handler to the form's onSubmit (and ensure the submit
button remains type="submit"); update the input to be controlled or read its
value in handleNewsletterSubmit if you plan to show it in the message.
In `@src/lib/better-stack.ts`:
- Around line 27-33: The stub in createLogger is being force-cast to Logtail;
instead extract a LoggerLike interface describing the used methods (debug, info,
warn, error, flush), type the noop stub as LoggerLike, and change createLogger's
return type from Logtail to LoggerLike (or union Logtail|LoggerLike) so callers
use the explicit lightweight logger interface; update any call sites that assume
Logtail accordingly (refer to createLogger, noop, and Logtail).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: def6ebe4-2e23-4137-87bc-faee85c8ebaf
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (16)
.github/workflows/ci.ymlbiome.jsoncontent/posts/building-for-zero-connectivity.mdxcontent/posts/introducing-resq-tactical-os.mdxpackage.jsonsrc/actions/contact/submit.tssrc/app/[locale]/(marketing)/blog/[slug]/page.tsxsrc/app/[locale]/(marketing)/blog/page.tsxsrc/app/[locale]/loading.tsxsrc/app/api/_elysia/shared/auth.tssrc/app/sitemap.tssrc/features/marketing/components/footer.tsxsrc/lib/better-stack.tssrc/lib/content.tssrc/utils/helpers.tssrc/utils/logger.ts
✅ Files skipped from review due to trivial changes (4)
- src/app/api/_elysia/shared/auth.ts
- biome.json
- src/utils/logger.ts
- content/posts/introducing-resq-tactical-os.mdx
🚧 Files skipped from review as they are similar to previous changes (5)
- package.json
- src/app/[locale]/(marketing)/blog/page.tsx
- src/app/sitemap.ts
- src/actions/contact/submit.ts
- src/lib/content.ts
| function formatDate(dateStr: string, locale: string) { | ||
| return new Date(dateStr).toLocaleDateString(locale, { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "numeric", | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In JavaScript/TypeScript, does new Date("YYYY-MM-DD")parse as UTC, and cantoLocaleDateString show the previous calendar day in negative UTC offsets (e.g., US time zones)? Please cite MDN/ECMAScript references.
💡 Result:
Yes, new Date("YYYY-MM-DD") parses the string as a date-only form in the Date Time String Format, which is interpreted as UTC time (midnight UTC on that date), per ECMAScript specification and MDN documentation. This is due to a historical spec decision for web compatibility, even though it differs from strict ISO 8601. Yes, toLocaleDateString can show the previous calendar day in negative UTC offsets (e.g., US time zones like EST/UTC-5). The Date object represents YYYY-MM-DD 00:00:00 UTC. When formatted in a local time zone behind UTC (negative offset), this UTC midnight converts to the previous day's evening (e.g., 2019-01-01T00:00:00Z becomes 2018-12-31 19:00:00 EST), so the date part is the prior day.
Citations:
- 1: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
- 3: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
- 4: https://stackoverflow.com/questions/32877278/tolocaledatestring-is-subtracting-a-day
- 5: https://tc39.es/ecma262/multipage/numbers-and-dates.html
- 6: http://developer.mozilla.org/en/docs/Core_JavaScript_1.5_Reference:Global_Objects:Date
🏁 Script executed:
cat -n src/app/[locale]/\(marketing\)/blog/\[slug\]/page.tsx | sed -n '25,45p'Repository: resq-software/landing
Length of output: 690
🏁 Script executed:
cat -n src/app/[locale]/\(marketing\)/blog/\[slug\]/page.tsx | head -35Repository: resq-software/landing
Length of output: 1324
🏁 Script executed:
cat -n src/app/[locale]/\(marketing\)/blog/\[slug\]/page.tsx | tail -30Repository: resq-software/landing
Length of output: 1437
🏁 Script executed:
rg -n "getPostBySlug\|post\.date" src/lib/content --type ts --type tsx -A 3 -B 1Repository: resq-software/landing
Length of output: 92
🏁 Script executed:
rg -n "getPostBySlug\|post\.date" src/lib/content -A 3 -B 1Repository: resq-software/landing
Length of output: 120
🏁 Script executed:
fd content -t d src/Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
rg -n "getPostBySlug" src/ -A 5Repository: resq-software/landing
Length of output: 1887
🏁 Script executed:
cat -n src/lib/content.ts | head -70Repository: resq-software/landing
Length of output: 2594
🏁 Script executed:
fd -e mdx content/posts | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 "{}"'Repository: resq-software/landing
Length of output: 422
🏁 Script executed:
fd . content/posts -e mdx | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -20 "{}"'Repository: resq-software/landing
Length of output: 3235
Avoid timezone drift when formatting YYYY-MM-DD post dates.
Line 33 parses a date-only string with new Date(dateStr), which is interpreted as UTC midnight. In browsers with negative UTC offsets (common in the US), toLocaleDateString converts this UTC time to the local date, displaying the previous day. Parse as UTC date parts and format with timeZone: "UTC" to preserve the intended date regardless of the user's timezone.
🛠️ Proposed fix
function formatDate(dateStr: string, locale: string) {
- return new Date(dateStr).toLocaleDateString(locale, {
- year: "numeric",
- month: "long",
- day: "numeric",
- });
+ const [year, month, day] = dateStr.split("-").map(Number);
+ const date = new Date(Date.UTC(year, month - 1, day));
+ return new Intl.DateTimeFormat(locale, {
+ year: "numeric",
+ month: "long",
+ day: "numeric",
+ timeZone: "UTC",
+ }).format(date);
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/(marketing)/blog/[slug]/page.tsx around lines 32 - 37, The
formatDate function currently uses new Date(dateStr) which treats a date-only
string as UTC midnight and then toLocaleDateString can shift it to the previous
day in negative-offset timezones; update formatDate to parse the YYYY-MM-DD
string into numeric year, month, and day (e.g., split on "-" and convert to
numbers), construct a Date using Date.UTC(year, month-1, day) or new
Date(Date.UTC(...)), and call toLocaleDateString with an explicit timeZone:
"UTC" option so the displayed date does not drift across timezones.
| document.body.style.overflow = 'hidden'; | ||
| document.body.setAttribute('aria-busy', 'true'); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Find all places in the codebase that manipulate document.body.style.overflow
# to assess the scope of potential conflicts.
rg -n "document\.body\.style\.overflow" --type=ts --type=tsx -C2Repository: resq-software/landing
Length of output: 92
🏁 Script executed:
#!/bin/bash
# Fix: Use -t ts flag (TypeScript covers both .ts and .tsx) or search without type filter
rg -n "document\.body\.style\.overflow" -t ts -C 2Repository: resq-software/landing
Length of output: 1668
🏁 Script executed:
#!/bin/bash
# Examine the loading.tsx file to understand the context and component structure
cat -n src/app/[locale]/loading.tsxRepository: resq-software/landing
Length of output: 5601
🏁 Script executed:
#!/bin/bash
# Examine the navbar.tsx file to understand the mobile menu management
cat -n src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 14705
Race condition with navbar and loading component both managing body scroll lock.
Both loading.tsx (lines 37, 49, 58) and navbar.tsx (lines 188, 190) independently manipulate document.body.style.overflow without coordination. Since the loading component runs for 30 seconds on mount, the mobile menu can be opened during this window. If the loading timeout fires while the menu is open, the component clears the overflow constraint, allowing the body to scroll behind the open menu.
Consider implementing a shared scroll lock manager or ref-counting approach to prevent conflicts between these components.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/loading.tsx around lines 37 - 38, The loading component in
loading.tsx and the navbar in navbar.tsx both directly set
document.body.style.overflow, causing a race when the loading timeout clears
overflow while the mobile menu is open; replace direct DOM writes with a shared
scroll-lock manager that provides reference-counted lock/unlock functions (e.g.,
acquireScrollLock/releaseScrollLock or lock/unlock) exported from a new module
and call those from the Loading component (on mount/timeout/cleanup) and from
the Navbar (when opening/closing the mobile menu) so each consumer
increments/decrements a counter and only clears overflow when the count reaches
zero; update Loading and Navbar to use these manager functions instead of
directly setting document.body.style.overflow or aria attributes.
| hideTimeout = setTimeout(() => { | ||
| setLoading(false); | ||
| document.body.style.overflow = ''; | ||
| document.body.removeAttribute('aria-busy'); | ||
| }, 30_000); // TODO: revert to 1200 after design review |
There was a problem hiding this comment.
Resolve the TODO before merging: 30-second delay is likely unintended for production.
The current 30,000ms (30 seconds) timeout will cause users to wait half a minute on the loading screen after the boot sequence completes. The TODO indicates this should be 1200ms.
If this is intentionally left for design review, consider gating it behind a development-only check to avoid accidentally shipping this to production.
Suggested fix
hideTimeout = setTimeout(() => {
setLoading(false);
document.body.style.overflow = '';
document.body.removeAttribute('aria-busy');
- }, 30_000); // TODO: revert to 1200 after design review
+ }, 1200);📝 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.
| hideTimeout = setTimeout(() => { | |
| setLoading(false); | |
| document.body.style.overflow = ''; | |
| document.body.removeAttribute('aria-busy'); | |
| }, 30_000); // TODO: revert to 1200 after design review | |
| hideTimeout = setTimeout(() => { | |
| setLoading(false); | |
| document.body.style.overflow = ''; | |
| document.body.removeAttribute('aria-busy'); | |
| }, 1200); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/loading.tsx around lines 47 - 51, The timeout for hiding
the loading state in the hideTimeout callback is set to 30_000ms (30s) but the
TODO says it should be 1_200ms; change the delay passed to setTimeout in the
hideTimeout setup to 1_200 (or wrap the 30_000 value behind a development-only
guard like process.env.NODE_ENV !== 'production' so production uses 1_200ms) and
ensure the rest of the callback still calls setLoading(false), resets
document.body.style.overflow, and removes the aria-busy attribute as implemented
in this component (the hideTimeout/setLoading/document.body.* logic).
Incorporated as a Delaware C Corp — update all copyright headers, legal pages, and config to reflect the official entity name. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 11
♻️ Duplicate comments (2)
src/app/[locale]/loading.tsx (2)
47-52:⚠️ Potential issue | 🟠 MajorReduce the post-boot delay for production.
Line 51 keeps users on the loading screen for 30 seconds, which is likely a production UX bug (the TODO already indicates
1200ms).Suggested change
- }, 30_000); // TODO: revert to 1200 after design review + }, process.env.NODE_ENV === 'production' ? 1_200 : 30_000);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/loading.tsx around lines 47 - 52, The timeout that clears the post-boot loading state uses 30_000ms causing an excessive 30s delay; change the setTimeout delay in the hideTimeout assignment from 30_000 to 1_200 (or 1200) so setLoading(false), document.body.style.overflow reset, and document.body.removeAttribute('aria-busy') run after ~1.2s instead of 30s (look for the hideTimeout = setTimeout(...) block).
37-39:⚠️ Potential issue | 🟡 MinorAvoid uncoordinated global scroll-lock writes.
This component directly mutates
document.body.style.overflow, andsrc/features/marketing/components/navbar.tsx(Lines 177-192) does the same. A release in either component can unlock scrolling while the other still needs it.Use a shared lock manager (ref-counted acquire/release) instead of direct writes in both places.
Also applies to: 49-50, 58-59
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/loading.tsx around lines 37 - 39, Replace direct mutations of document.body.style.overflow in the Loading component and navbar by using a shared ref-counted lock manager: create a small module exporting acquireScrollLock() and releaseScrollLock() (names must match imports in src/app/[locale]/loading.tsx and src/features/marketing/components/navbar.tsx), implement it so the first acquire sets document.body.style.overflow = 'hidden' and subsequent acquires increment a counter, and release decrements the counter and only restores overflow when count reaches zero; update loading.tsx to call acquireScrollLock() when mounted and releaseScrollLock() on unmount (replace the direct document.body writes), and do the same in navbar.tsx so both components coordinate the scroll lock.
🧹 Nitpick comments (4)
src/app/[locale]/(marketing)/contact/page.tsx (2)
116-129: Consider usingappconstants instead of hardcoded company info.The company info bar duplicates values already defined in
app.company(legal,stage,locations). Using the constants would ensure consistency and single-source-of-truth.♻️ Suggested change
<section aria-label="Company information" className="border-t border-border-default bg-bg-secondary py-8"> <div className="mx-auto flex max-w-6xl flex-wrap items-center justify-center gap-x-6 gap-y-2 px-6"> <span className="font-mono text-xs uppercase tracking-widest text-text-muted"> - ResQ Systems, Inc. + {app.company.legal} </span> <span className="text-text-muted" aria-hidden="true">|</span> <span className="font-mono text-xs uppercase tracking-widest text-text-muted"> - Seed Stage · Q1 2026 + {app.company.stage} · Q1 2026 </span> <span className="text-text-muted" aria-hidden="true">|</span> <span className="font-mono text-xs uppercase tracking-widest text-text-muted"> - New York + Texas + {app.company.locations.join(" + ")} </span> </div> </section>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/contact/page.tsx around lines 116 - 129, The company info section currently hardcodes company details; replace those literals with the centralized constants from app.company (use app.company.legal, app.company.stage, app.company.locations) so the bar reads the single source of truth; import or reference the existing app.company object where page.tsx renders the <section> and substitute the three spans (legal, stage, locations) to use those properties while preserving the separators and accessibility attributes.
11-11: Usingnext/linkfor external URLs is unconventional.The
contactLinksarray only contains external URLs (docs, GitHub, NuGet, PyPI). Whilenext/linkcan handle external URLs, using a plain<a>tag is more idiomatic for external navigation since Next.js Link is optimized for client-side routing within the app.♻️ Suggested change
-import Link from "next/link"; +// No Link import needed - using <a> for external links ... - <Link + <a key={link.label} href={link.href} target="_blank" rel="noopener noreferrer" className="group flex flex-col gap-3 border border-border-default bg-bg-card p-4 transition-opacity hover:opacity-80" > ... - </Link> + </a>Also applies to: 85-103
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/app/`[locale]/(marketing)/contact/page.tsx at line 11, The file imports Link from "next/link" and uses it to render external URLs from the contactLinks array; replace those Link usages with plain anchor elements (<a>) using target="_blank" and rel="noopener noreferrer" for safety, and remove the unused Link import. Locate the contactLinks array and each JSX rendering that references Link (around the block where Link is imported and the map rendering lines ~85-103) and change to <a href={url} ...>text</a> while preserving styling and accessibility attributes.src/features/marketing/sections/footer-cta.tsx (1)
66-70: Consider using CSS variables instead of hardcoded colors.The input border (
#4A5272) and button background (#C13130) use hardcoded hex values while other elements use CSS variables (bg-bg-card,text-text-primary, etc.). For theme consistency, consider using the existing design tokens.♻️ Suggested change
- className="min-w-0 flex-1 border border-[`#4A5272`] bg-bg-card px-4 py-4 font-mono text-sm uppercase tracking-wider text-text-primary placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-blue" + className="min-w-0 flex-1 border border-border-default bg-bg-card px-4 py-4 font-mono text-sm uppercase tracking-wider text-text-primary placeholder:text-text-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-blue" /> <button type="submit" - className="bg-[`#C13130`] px-8 py-4 font-mono text-sm font-semibold uppercase tracking-widest text-white motion-safe:transition-colors motion-safe:duration-200 hover:brightness-110" + className="bg-accent-red px-8 py-4 font-mono text-sm font-semibold uppercase tracking-widest text-white motion-safe:transition-colors motion-safe:duration-200 hover:brightness-110"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/features/marketing/sections/footer-cta.tsx` around lines 66 - 70, The input and submit button are using hardcoded hex colors (border `#4A5272` and button bg `#C13130`); update the className on the input element and the button element in footer-cta.tsx to use the existing CSS design tokens instead (e.g., replace border-[`#4A5272`] with the project border color variable like border-border-muted or similar, and replace bg-[`#C13130`] with the primary/accent button background token such as bg-accent-red or bg-primary) so the component matches the other utility classes (bg-bg-card, text-text-primary) and supports theming consistently.src/proxy.ts (1)
107-107: Avoid splitting the skip rules betweenmatcherandshouldBypassProxy().Line 107 now hard-excludes cases that
shouldBypassProxy()already filters, while other skips still live only in the runtime helper. That overlap makes some branches unreachable and makes future routing changes easy to miss.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/proxy.ts` at line 107, The matcher currently hard-excludes routes with the negative lookahead while shouldBypassProxy() also filters those same cases, causing overlapping/unreachable branches; consolidate skip rules into one place by removing the negative lookahead from matcher (change matcher to a broader pattern, e.g. String.raw`/(.*)/` or the intended non-exclusion pattern) and ensure shouldBypassProxy() contains the full canonical list of skip conditions (api, trpc, _next, _vercel, files with extensions, etc.), or alternatively move all skip logic into matcher and simplify shouldBypassProxy() to only runtime-only checks — update the matcher and the shouldBypassProxy() function so all skip rules live in the same single source of truth.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/app/`[locale]/(marketing)/blog/page.tsx:
- Around line 17-23: The formatDate function suffers from timezone drift because
new Date(dateStr) treats YYYY-MM-DD as UTC midnight and negative offsets can
render the previous day; update formatDate to parse the dateStr (e.g.,
split/regex for year, month, day) and construct the Date using Date.UTC with a
safe midday time (e.g., new Date(Date.UTC(year, month-1, day, 12, 0, 0))) before
calling toLocaleDateString(locale, ...); this change in the formatDate function
prevents local timezone offsets from shifting the displayed calendar day.
In `@src/features/marketing/components/navbar.tsx`:
- Around line 203-205: The nav element's aria-label is hardcoded ("Main
navigation"); add a key (e.g., "mainNavigation") to the Nav i18n catalog with
translated values and use the same translation lookup used for other menu
strings in this component (the Nav/navbar component) to set aria-label
dynamically; update the nav JSX to call the translation function (the same
helper used for other labels in this file) and pass the localized string into
aria-label so screen readers receive the translated landmark label.
- Around line 24-50: NavListItem currently always renders a raw <a>, causing
internal links (e.g., items returned by usePlatformItems like "/platform" and
"/protocol") to do full-page navigations; update NavListItem to mirror the
mobile menu logic by importing Link from "next/link" and conditionally rendering
<Link href={href}>...children...</Link> for non-external/internal hrefs and
falling back to a plain <a href={href} ...> for external URLs, preserving
existing props (target, rel, className) and accessibility attributes; ensure any
external detection helper used in the mobile menu (or equivalent logic) is
reused so internal routes use client-side navigation.
- Around line 303-361: Replace the current manual mobile menu DOM (the div with
id="mobile-menu" that uses inert={!menuOpen}, menuOpen state, and setMenuOpen)
with a Radix Dialog primitive: wrap the menu in Dialog.Root open={menuOpen}
onOpenChange={setMenuOpen}, render the backdrop via Dialog.Overlay and the menu
contents inside Dialog.Content (or Dialog.Portal > Dialog.Content) so Radix
handles focus trapping and Escape/close behavior; remove the inert prop and
existing manual focus logic/escape handlers. Keep the existing mapping over
mobileLinks (including external link handling, target/rel, icon and sr-only
text), the mobile CTA Link (href="/contact") and LocaleSwitcher inside the
Dialog.Content, and import/use Dialog, DialogOverlay, DialogContent, DialogClose
from `@radix-ui/react-dialog` as needed. Ensure styling/classes from the original
container are applied to Dialog.Content and that setMenuOpen(false) is preserved
for link onClick to close the dialog.
In `@src/features/marketing/sections/architecture-diagram.tsx`:
- Line 95: The table element in the ArchitectureDiagram component uses a
non-existent Tailwind class "min-w-125" (seen in the table's className "w-full
min-w-125"), which produces no CSS; either replace that token with an arbitrary
Tailwind value like "min-w-[500px]" in the table's className (and likewise in
technology.tsx where used) or add "min-w-125" to your Tailwind theme scale
inside the `@theme` inline block in src/styles/globals.css so the class is
generated.
In `@src/features/marketing/sections/footer-cta.tsx`:
- Around line 47-50: The footer CTA currently uses a mailto form (<form
action="mailto:...">) but displays a misleading "ENCRYPTED TRANSMISSION //
256-BIT SSL" claim; either remove/reword that security text or replace the
mailto form with the existing ContactForm component that calls the submitContact
server action to perform secure submission. Locate the <form
action="mailto:contact@resq.software" ...> in footer-cta.tsx (and the duplicate
instance) and either: 1) delete or change the security copy to a non-technical,
accurate phrase (e.g., "Contact via your email client") or 2) swap the form
markup for the ContactForm component and wire it to submitContact so submissions
go through the server action rather than mailto.
In `@src/features/marketing/sections/hero.tsx`:
- Around line 13-17: SCENARIOS[videoIndex] can be undefined causing HeroHud to
read properties off undefined; clamp or defensively resolve the scenario before
lookup: compute an index = Math.max(0, Math.min(videoIndex, SCENARIOS.length -
1)) or select a fallback scenario when SCENARIOS[videoIndex] is undefined, then
pass that safe scenario into HeroHud and any other places referencing
SCENARIOS[videoIndex] (e.g., where videoIndex is used to read uptimeBase and
location), and apply the same defensive lookup for the other occurrences noted
(the other SCENARIOS access points).
In `@src/lib/content.ts`:
- Around line 59-62: getPostBySlug currently uses the incoming slug directly in
path.join which allows path traversal; validate or sanitize the slug before
building filePath (e.g., enforce an allowlist regex like only
letters/numbers/hyphens/underscores or compute resolved =
path.resolve(POSTS_DIR, `${slug}.mdx`) and ensure
resolved.startsWith(path.resolve(POSTS_DIR))). If validation fails, return null
(or throw) and only then read the file; update references to getPostBySlug,
POSTS_DIR, and filePath accordingly.
In `@src/proxy.ts`:
- Around line 92-99: The code sets the nonce and CSP only on the response, but
Next.js requires the CSP header (with nonce) on the forwarded request so SSR can
inject nonce attributes; update the flow around intlMiddleware and
applySecurityHeaders to add the nonce and CSP into the outgoing request headers
(e.g., build or mutate requestHeaders and call NextResponse.next({ request: {
headers: requestHeaders } })) rather than only setting them on the response, or
ensure applySecurityHeaders writes both request and response headers; modify
where nonce is generated (keep using crypto.randomUUID().replaceAll("-", ""))
and ensure the same nonce is applied to the CSP header and forwarded on the
request before intlMiddleware/NextResponse.next is invoked.
In `@src/utils/helpers.ts`:
- Around line 75-78: The code reads VITE_BASE_URL / NEXT_PUBLIC_BASE_URL /
BASE_URL directly from process.env (and uses optional chaining on process) which
violates the env validation rule; add VITE_BASE_URL, NEXT_PUBLIC_BASE_URL, and
BASE_URL to the clientEnvSchema in src/env.ts (so they are validated and exposed
on the client as needed), export the validated env, then replace the direct
process?.env access in the helper with the imported validated env (e.g., use
env.VITE_BASE_URL || env.NEXT_PUBLIC_BASE_URL || env.BASE_URL) and remove the
optional chaining on process.
---
Duplicate comments:
In `@src/app/`[locale]/loading.tsx:
- Around line 47-52: The timeout that clears the post-boot loading state uses
30_000ms causing an excessive 30s delay; change the setTimeout delay in the
hideTimeout assignment from 30_000 to 1_200 (or 1200) so setLoading(false),
document.body.style.overflow reset, and
document.body.removeAttribute('aria-busy') run after ~1.2s instead of 30s (look
for the hideTimeout = setTimeout(...) block).
- Around line 37-39: Replace direct mutations of document.body.style.overflow in
the Loading component and navbar by using a shared ref-counted lock manager:
create a small module exporting acquireScrollLock() and releaseScrollLock()
(names must match imports in src/app/[locale]/loading.tsx and
src/features/marketing/components/navbar.tsx), implement it so the first acquire
sets document.body.style.overflow = 'hidden' and subsequent acquires increment a
counter, and release decrements the counter and only restores overflow when
count reaches zero; update loading.tsx to call acquireScrollLock() when mounted
and releaseScrollLock() on unmount (replace the direct document.body writes),
and do the same in navbar.tsx so both components coordinate the scroll lock.
---
Nitpick comments:
In `@src/app/`[locale]/(marketing)/contact/page.tsx:
- Around line 116-129: The company info section currently hardcodes company
details; replace those literals with the centralized constants from app.company
(use app.company.legal, app.company.stage, app.company.locations) so the bar
reads the single source of truth; import or reference the existing app.company
object where page.tsx renders the <section> and substitute the three spans
(legal, stage, locations) to use those properties while preserving the
separators and accessibility attributes.
- Line 11: The file imports Link from "next/link" and uses it to render external
URLs from the contactLinks array; replace those Link usages with plain anchor
elements (<a>) using target="_blank" and rel="noopener noreferrer" for safety,
and remove the unused Link import. Locate the contactLinks array and each JSX
rendering that references Link (around the block where Link is imported and the
map rendering lines ~85-103) and change to <a href={url} ...>text</a> while
preserving styling and accessibility attributes.
In `@src/features/marketing/sections/footer-cta.tsx`:
- Around line 66-70: The input and submit button are using hardcoded hex colors
(border `#4A5272` and button bg `#C13130`); update the className on the input
element and the button element in footer-cta.tsx to use the existing CSS design
tokens instead (e.g., replace border-[`#4A5272`] with the project border color
variable like border-border-muted or similar, and replace bg-[`#C13130`] with the
primary/accent button background token such as bg-accent-red or bg-primary) so
the component matches the other utility classes (bg-bg-card, text-text-primary)
and supports theming consistently.
In `@src/proxy.ts`:
- Line 107: The matcher currently hard-excludes routes with the negative
lookahead while shouldBypassProxy() also filters those same cases, causing
overlapping/unreachable branches; consolidate skip rules into one place by
removing the negative lookahead from matcher (change matcher to a broader
pattern, e.g. String.raw`/(.*)/` or the intended non-exclusion pattern) and
ensure shouldBypassProxy() contains the full canonical list of skip conditions
(api, trpc, _next, _vercel, files with extensions, etc.), or alternatively move
all skip logic into matcher and simplify shouldBypassProxy() to only
runtime-only checks — update the matcher and the shouldBypassProxy() function so
all skip rules live in the same single source of truth.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 121bb60c-40a6-4aea-8b4b-c82c96f100b8
⛔ Files ignored due to path filters (1)
public/assets/images/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (118)
.git-hooks/commit-msg.git-hooks/post-checkout.git-hooks/post-merge.git-hooks/pre-commit.git-hooks/pre-push.git-hooks/prepare-commit-msg.github/labeler.yml.github/labels.yml.github/skills/feynman-auditor/SKILL.md.github/skills/nemesis-auditor/SKILL.md.github/skills/state-inconsistency-auditor/SKILL.md.github/workflows/auto-merge.yml.github/workflows/ci.yml.github/workflows/deploy.yml.github/workflows/label-sync.yml.github/workflows/labeler.yml.github/workflows/pr-size-labeler.ymlDockerfileagent-sync.shcustom.d.tsflake.nixpublic/assets/models/README.mdscripts/lib/shell-utils.shscripts/setup.shsrc/actions/contact/submit.tssrc/app/[locale]/(marketing)/blog/[slug]/page.tsxsrc/app/[locale]/(marketing)/blog/page.tsxsrc/app/[locale]/(marketing)/contact/page.tsxsrc/app/[locale]/(marketing)/layout.tsxsrc/app/[locale]/(marketing)/legal/acceptable-use/page.tsxsrc/app/[locale]/(marketing)/legal/cookie-policy/page.tsxsrc/app/[locale]/(marketing)/legal/data-processing/page.tsxsrc/app/[locale]/(marketing)/legal/layout.tsxsrc/app/[locale]/(marketing)/legal/page.tsxsrc/app/[locale]/(marketing)/legal/privacy/page.tsxsrc/app/[locale]/(marketing)/legal/security/page.tsxsrc/app/[locale]/(marketing)/legal/terms/page.tsxsrc/app/[locale]/(marketing)/page.tsxsrc/app/[locale]/(marketing)/platform/page.tsxsrc/app/[locale]/(marketing)/protocol/page.tsxsrc/app/[locale]/forbidden.tsxsrc/app/[locale]/layout.tsxsrc/app/[locale]/loading.tsxsrc/app/_client/errors/401.tsxsrc/app/_client/errors/403.tsxsrc/app/_client/errors/404.tsxsrc/app/_client/errors/500.tsxsrc/app/_client/errors/503.tsxsrc/app/_client/errors/error-layout.tsxsrc/app/_client/errors/index.tssrc/app/_client/index.tssrc/app/_components/index.tssrc/app/_components/pagination/index.tssrc/app/_components/pagination/pagination.tsxsrc/app/_components/vitals/index.tssrc/app/_components/vitals/web-vitals.tsxsrc/app/_providers/index.tssrc/app/_providers/main-provider.tsxsrc/app/api/[[...route]]/elysia.tssrc/app/api/[[...route]]/health/handlers.tssrc/app/api/[[...route]]/health/index.tssrc/app/api/[[...route]]/health/middleware.tssrc/app/api/[[...route]]/health/schema.tssrc/app/api/[[...route]]/route.tssrc/app/api/[[...route]]/status/handlers.tssrc/app/api/_elysia/constants/index.tssrc/app/api/_elysia/index.tssrc/app/api/_elysia/shared/auth.tssrc/app/api/_elysia/shared/config.tssrc/app/api/_elysia/shared/index.tssrc/app/api/_elysia/shared/middleware.tssrc/app/layout.tsxsrc/app/manifest.tssrc/constants/app.tssrc/features/marketing/components/background.tsxsrc/features/marketing/components/contact-form.tsxsrc/features/marketing/components/footer.tsxsrc/features/marketing/components/hash-scroll-center.tsxsrc/features/marketing/components/hero-hud.tsxsrc/features/marketing/components/hero-video-slideshow.tsxsrc/features/marketing/components/locale-switcher.tsxsrc/features/marketing/components/navbar.tsxsrc/features/marketing/components/status-badge.tsxsrc/features/marketing/components/use-case-tabs.tsxsrc/features/marketing/sections/architecture-diagram.tsxsrc/features/marketing/sections/competitive-table.tsxsrc/features/marketing/sections/footer-cta.tsxsrc/features/marketing/sections/hero.tsxsrc/features/marketing/sections/how-it-works.tsxsrc/features/marketing/sections/index.tssrc/features/marketing/sections/metrics-strip.tsxsrc/features/marketing/sections/platform-pillars.tsxsrc/features/marketing/sections/problem-statement.tsxsrc/features/marketing/sections/technology.tsxsrc/features/marketing/sections/trust-security.tsxsrc/features/marketing/sections/use-cases.tsxsrc/features/marketing/sections/why-now.tsxsrc/hooks/use-in-view.tssrc/i18n/navigation.tssrc/i18n/request.tssrc/i18n/routing.tssrc/lib/better-stack.tssrc/lib/content.tssrc/lib/utils.test.tssrc/mdx-components.tsxsrc/providers/core/events.tsxsrc/providers/core/index.tssrc/providers/core/tailwind-indicator.tsxsrc/providers/global/index.tssrc/providers/global/jotai-provider.tsxsrc/providers/index.tssrc/providers/providers.tsxsrc/proxy.tssrc/utils/helpers.test.tssrc/utils/helpers.tssrc/utils/logger.test.tssrc/utils/seo/meta.test.tssrc/utils/seo/speculation-schema.test.ts
✅ Files skipped from review due to trivial changes (82)
- .github/labels.yml
- flake.nix
- .git-hooks/commit-msg
- .github/workflows/auto-merge.yml
- .github/skills/state-inconsistency-auditor/SKILL.md
- .git-hooks/prepare-commit-msg
- scripts/lib/shell-utils.sh
- scripts/setup.sh
- .git-hooks/pre-push
- src/app/_components/pagination/index.ts
- src/app/_client/index.ts
- .git-hooks/post-checkout
- .github/workflows/pr-size-labeler.yml
- src/app/api/[[...route]]/health/schema.ts
- Dockerfile
- public/assets/models/README.md
- src/features/marketing/components/background.tsx
- .github/labeler.yml
- .git-hooks/pre-commit
- .github/workflows/labeler.yml
- src/app/api/[[...route]]/elysia.ts
- .github/skills/nemesis-auditor/SKILL.md
- custom.d.ts
- .github/workflows/label-sync.yml
- src/app/_components/vitals/index.ts
- .github/workflows/ci.yml
- src/providers/index.ts
- src/app/api/_elysia/constants/index.ts
- .github/workflows/deploy.yml
- src/app/api/[[...route]]/health/middleware.ts
- src/features/marketing/components/status-badge.tsx
- src/app/_client/errors/401.tsx
- src/app/_providers/index.ts
- src/app/_client/errors/503.tsx
- src/app/_client/errors/error-layout.tsx
- src/app/_components/pagination/pagination.tsx
- src/app/api/_elysia/shared/index.ts
- src/app/_providers/main-provider.tsx
- src/app/_client/errors/500.tsx
- src/app/_client/errors/403.tsx
- src/providers/global/jotai-provider.tsx
- src/features/marketing/components/use-case-tabs.tsx
- .git-hooks/post-merge
- src/app/api/_elysia/index.ts
- src/app/api/[[...route]]/route.ts
- src/app/_client/errors/index.ts
- src/providers/global/index.ts
- src/app/api/_elysia/shared/config.ts
- src/app/api/[[...route]]/status/handlers.ts
- src/providers/core/index.ts
- agent-sync.sh
- src/app/api/_elysia/shared/auth.ts
- .github/skills/feynman-auditor/SKILL.md
- src/app/api/_elysia/shared/middleware.ts
- src/app/_components/vitals/web-vitals.tsx
- src/providers/core/events.tsx
- src/features/marketing/sections/how-it-works.tsx
- src/hooks/use-in-view.ts
- src/features/marketing/sections/index.ts
- src/app/api/[[...route]]/health/handlers.ts
- src/mdx-components.tsx
- src/providers/providers.tsx
- src/providers/core/tailwind-indicator.tsx
- src/lib/utils.test.ts
- src/app/[locale]/(marketing)/legal/acceptable-use/page.tsx
- src/app/_client/errors/404.tsx
- src/app/[locale]/(marketing)/legal/privacy/page.tsx
- src/i18n/routing.ts
- src/i18n/request.ts
- src/app/api/[[...route]]/health/index.ts
- src/app/[locale]/(marketing)/legal/terms/page.tsx
- src/app/[locale]/(marketing)/layout.tsx
- src/app/[locale]/(marketing)/protocol/page.tsx
- src/app/[locale]/(marketing)/platform/page.tsx
- src/features/marketing/sections/trust-security.tsx
- src/utils/helpers.test.ts
- src/app/[locale]/(marketing)/legal/page.tsx
- src/utils/seo/speculation-schema.test.ts
- src/features/marketing/sections/why-now.tsx
- src/utils/seo/meta.test.ts
- src/lib/better-stack.ts
- src/app/_components/index.ts
🚧 Files skipped from review as they are similar to previous changes (22)
- src/app/[locale]/forbidden.tsx
- src/app/[locale]/(marketing)/legal/layout.tsx
- src/app/[locale]/(marketing)/page.tsx
- src/app/manifest.ts
- src/actions/contact/submit.ts
- src/i18n/navigation.ts
- src/features/marketing/components/hash-scroll-center.tsx
- src/app/[locale]/(marketing)/legal/cookie-policy/page.tsx
- src/app/layout.tsx
- src/features/marketing/components/contact-form.tsx
- src/features/marketing/components/hero-video-slideshow.tsx
- src/features/marketing/sections/use-cases.tsx
- src/features/marketing/sections/competitive-table.tsx
- src/constants/app.ts
- src/features/marketing/sections/metrics-strip.tsx
- src/app/[locale]/(marketing)/legal/data-processing/page.tsx
- src/app/[locale]/(marketing)/legal/security/page.tsx
- src/features/marketing/sections/platform-pillars.tsx
- src/features/marketing/sections/technology.tsx
- src/features/marketing/components/locale-switcher.tsx
- src/utils/logger.test.ts
- src/features/marketing/components/footer.tsx
| function formatDate(dateStr: string, locale: string) { | ||
| return new Date(dateStr).toLocaleDateString(locale, { | ||
| year: "numeric", | ||
| month: "long", | ||
| day: "numeric", | ||
| }); | ||
| } |
There was a problem hiding this comment.
Timezone drift when formatting YYYY-MM-DD dates.
Same issue as in the blog post page: new Date(dateStr) parses date-only strings as UTC midnight. In negative UTC offset timezones (e.g., US), toLocaleDateString may display the previous calendar day.
🛠️ Proposed fix
function formatDate(dateStr: string, locale: string) {
- return new Date(dateStr).toLocaleDateString(locale, {
+ const [year, month, day] = dateStr.split("-").map(Number);
+ const date = new Date(Date.UTC(year, month - 1, day));
+ return new Intl.DateTimeFormat(locale, {
year: "numeric",
month: "long",
day: "numeric",
- });
+ timeZone: "UTC",
+ }).format(date);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function formatDate(dateStr: string, locale: string) { | |
| return new Date(dateStr).toLocaleDateString(locale, { | |
| year: "numeric", | |
| month: "long", | |
| day: "numeric", | |
| }); | |
| } | |
| function formatDate(dateStr: string, locale: string) { | |
| const [year, month, day] = dateStr.split("-").map(Number); | |
| const date = new Date(Date.UTC(year, month - 1, day)); | |
| return new Intl.DateTimeFormat(locale, { | |
| year: "numeric", | |
| month: "long", | |
| day: "numeric", | |
| timeZone: "UTC", | |
| }).format(date); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/`[locale]/(marketing)/blog/page.tsx around lines 17 - 23, The
formatDate function suffers from timezone drift because new Date(dateStr) treats
YYYY-MM-DD as UTC midnight and negative offsets can render the previous day;
update formatDate to parse the dateStr (e.g., split/regex for year, month, day)
and construct the Date using Date.UTC with a safe midday time (e.g., new
Date(Date.UTC(year, month-1, day, 12, 0, 0))) before calling
toLocaleDateString(locale, ...); this change in the formatDate function prevents
local timezone offsets from shifting the displayed calendar day.
| function usePlatformItems() { | ||
| const t = useTranslations("Nav.platformItems"); | ||
| return useMemo( | ||
| () => [ | ||
| { | ||
| title: t("overview"), | ||
| href: "/platform", | ||
| description: t("overviewDesc"), | ||
| }, | ||
| { | ||
| title: t("protocol"), | ||
| href: "/protocol", | ||
| description: t("protocolDesc"), | ||
| }, | ||
| { | ||
| title: t("technology"), | ||
| href: "/platform#technology", | ||
| description: t("technologyDesc"), | ||
| }, | ||
| { | ||
| title: t("useCases"), | ||
| href: "/platform#use-cases", | ||
| description: t("useCasesDesc"), | ||
| }, | ||
| ], | ||
| [t], | ||
| ); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the navbar.tsx file to understand the structure
wc -l src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 113
🏁 Script executed:
# Read the entire navbar.tsx to understand the NavListItem and link handling
cat -n src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 14704
🏁 Script executed:
# Search for how internal links are handled elsewhere in the codebase
rg -A 5 "from 'next/link'" src/features/marketing/components/Repository: resq-software/landing
Length of output: 47
Desktop platform links should use Next.js Link for internal routes.
NavListItem always renders a raw <a> element for all routes. Platform items like /platform and /protocol bypass the app router, causing full-page navigations instead of client-side routing. The mobile menu correctly handles this (lines 316–343) by conditionally selecting between Link and <a>. Update NavListItem to follow the same pattern: use Link for non-external hrefs and <a> for external ones.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/components/navbar.tsx` around lines 24 - 50,
NavListItem currently always renders a raw <a>, causing internal links (e.g.,
items returned by usePlatformItems like "/platform" and "/protocol") to do
full-page navigations; update NavListItem to mirror the mobile menu logic by
importing Link from "next/link" and conditionally rendering <Link
href={href}>...children...</Link> for non-external/internal hrefs and falling
back to a plain <a href={href} ...> for external URLs, preserving existing props
(target, rel, className) and accessibility attributes; ensure any external
detection helper used in the mobile menu (or equivalent logic) is reused so
internal routes use client-side navigation.
| <nav | ||
| aria-label="Main navigation" | ||
| className="container flex h-16 items-center justify-between md:h-20" | ||
| className="container flex h-16 items-center justify-between px-6 py-2 md:h-20 md:py-3" |
There was a problem hiding this comment.
Localize the navigation landmark label too.
aria-label="Main navigation" stays English on every locale. Please move it into the Nav catalog with the other menu strings so screen readers get the translated label as well.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/components/navbar.tsx` around lines 203 - 205, The nav
element's aria-label is hardcoded ("Main navigation"); add a key (e.g.,
"mainNavigation") to the Nav i18n catalog with translated values and use the
same translation lookup used for other menu strings in this component (the
Nav/navbar component) to set aria-label dynamically; update the nav JSX to call
the translation function (the same helper used for other labels in this file)
and pass the localized string into aria-label so screen readers receive the
translated landmark label.
| {/* Full-screen mobile menu (Skydio-style) */} | ||
| <div | ||
| id="mobile-menu" | ||
| inert={!menuOpen} | ||
| className={cn( | ||
| "grid border-t border-border-default/50 bg-bg-primary/98 backdrop-blur-xl transition-all duration-300 lg:hidden", | ||
| "fixed inset-0 top-16 z-40 flex flex-col bg-bg-primary transition-all duration-300 md:top-20 lg:hidden", | ||
| menuOpen | ||
| ? "grid-rows-[1fr] opacity-100" | ||
| : "grid-rows-[0fr] border-t-0 opacity-0", | ||
| ? "visible translate-y-0 opacity-100" | ||
| : "invisible -translate-y-4 opacity-0", | ||
| )} | ||
| > | ||
| <div className="overflow-hidden"> | ||
| <div className="container flex flex-col gap-1 py-4"> | ||
| {MOBILE_LINKS.map((link) => ( | ||
| <a | ||
| key={link.label} | ||
| href={link.href} | ||
| className="px-4 py-3 font-mono text-sm uppercase tracking-wider text-text-secondary transition-colors hover:text-text-primary" | ||
| onClick={() => setMenuOpen(false)} | ||
| {...(link.href.startsWith("http") | ||
| ? { target: "_blank", rel: "noopener noreferrer" } | ||
| : {})} | ||
| > | ||
| {link.label} | ||
| {link.href.startsWith("http") && ( | ||
| <span className="ml-1.5 text-text-muted">↗</span> | ||
| )} | ||
| </a> | ||
| ))} | ||
| <div className="mt-3 border-t border-border-default pt-4"> | ||
| <Link | ||
| href="/contact" | ||
| className="block w-full border border-accent-red bg-accent-red/10 px-5 py-3 text-center font-mono text-xs font-bold uppercase tracking-widest text-accent-red transition-colors hover:bg-accent-red/20" | ||
| onClick={() => setMenuOpen(false)} | ||
| > | ||
| Mission Control | ||
| </Link> | ||
| </div> | ||
| <div className="flex-1 overflow-y-auto px-6 py-6"> | ||
| <div className="flex flex-col divide-y divide-border-default/30"> | ||
| {mobileLinks.map((link) => { | ||
| const external = link.href.startsWith("http"); | ||
| const El = external ? "a" : Link; | ||
| return ( | ||
| <El | ||
| key={link.label} | ||
| href={link.href} | ||
| className="py-4 font-display text-xl font-bold uppercase tracking-wide text-text-primary transition-colors hover:text-accent-red sm:text-2xl" | ||
| onClick={() => setMenuOpen(false)} | ||
| {...(external | ||
| ? { target: "_blank", rel: "noopener noreferrer" } | ||
| : {})} | ||
| > | ||
| {link.label} | ||
| {external && ( | ||
| <> | ||
| <span | ||
| className="ml-2 text-text-muted" | ||
| aria-hidden="true" | ||
| > | ||
| ↗ | ||
| </span> | ||
| <span className="sr-only"> {t("opensInNewTab")}</span> | ||
| </> | ||
| )} | ||
| </El> | ||
| ); | ||
| })} | ||
| </div> | ||
|
|
||
| {/* Mobile CTA */} | ||
| <div className="mt-8 sm:hidden"> | ||
| <Link | ||
| href="/contact" | ||
| className="block w-full border border-accent-red bg-accent-red/10 px-5 py-3.5 text-center font-mono text-xs font-bold uppercase tracking-widest text-accent-red transition-colors hover:bg-accent-red/20" | ||
| onClick={() => setMenuOpen(false)} | ||
| > | ||
| {t("missionControl")} | ||
| </Link> | ||
| </div> | ||
| </div> | ||
|
|
||
| {/* Mobile locale switcher at bottom */} | ||
| <div className="border-t border-border-default/30 px-6 py-6"> | ||
| <LocaleSwitcher variant="full" /> | ||
| </div> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# First, let's examine the navbar component and understand the full context
wc -l src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 113
🏁 Script executed:
# Read the navbar component to see the full implementation
cat -n src/features/marketing/components/navbar.tsx | head -100Repository: resq-software/landing
Length of output: 3162
🏁 Script executed:
# Check if there's an overlay or backdrop element before the mobile menu
cat -n src/features/marketing/components/navbar.tsx | sed -n '280,365p'Repository: resq-software/landing
Length of output: 3989
🏁 Script executed:
# Check if Radix Dialog is in dependencies
fd -e json -e lock | xargs grep -l "radix-ui.*dialog\|@radix-ui/dialog" 2>/dev/null | head -5Repository: resq-software/landing
Length of output: 73
🏁 Script executed:
# Search for Dialog usage in the codebase to understand patterns
rg -t tsx -t ts "Dialog|Sheet|Modal" src/ --max-count=20Repository: resq-software/landing
Length of output: 92
🏁 Script executed:
# Search for Dialog/Modal/Sheet usage patterns in the codebase
rg "Dialog|Sheet|Modal" src/ --max-count=20Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
# Check the bun.lock file for Radix UI dependencies
grep -A 2 -B 2 "radix-ui" bun.lock | head -50Repository: resq-software/landing
Length of output: 10758
🏁 Script executed:
# Check the package.json for dependencies
cat package.json | grep -A 20 '"dependencies"'Repository: resq-software/landing
Length of output: 845
🏁 Script executed:
# Look for any custom focus trap or modal utilities
fd -e ts -e tsx src/lib src/utils | xargs rg -l "focus.*trap|modal|dialog" 2>/dev/nullRepository: resq-software/landing
Length of output: 439
🏁 Script executed:
# Check if there's a backdrop overlay element in the navbar
rg "backdrop|overlay" src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 323
🏁 Script executed:
# Check the beginning of the navbar component to see the full structure
cat -n src/features/marketing/components/navbar.tsx | sed -n '100,200p'Repository: resq-software/landing
Length of output: 3656
🏁 Script executed:
# Look for any backdrop/overlay element elsewhere in the navbar
cat -n src/features/marketing/components/navbar.tsx | sed -n '240,305p'Repository: resq-software/landing
Length of output: 3204
🏁 Script executed:
# Check if there's any focus management or scroll prevention code
rg "useEffect|useFocusTrap|removeScroll|focusScope" src/features/marketing/components/navbar.tsxRepository: resq-software/landing
Length of output: 198
🏁 Script executed:
# Verify Radix Dialog is properly available and check version
cat package.json | grep -i "@radix-ui/react-dialog"Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
# Check what's in `@resq-sw/ui` to understand custom component availability
cat package.json | grep "@resq-sw/ui"Repository: resq-software/landing
Length of output: 93
🏁 Script executed:
# Look for any Sheet/Dialog implementations in src
find src -type f -name "*.tsx" -o -name "*.ts" | head -20Repository: resq-software/landing
Length of output: 947
🏁 Script executed:
# Check if there are any custom modal/dialog utilities
fd -type f -path "*/src/lib/*" -o -path "*/src/components/*" | xargs rg "Dialog|Modal|Sheet" 2>/dev/null | head -20Repository: resq-software/landing
Length of output: 235
🏁 Script executed:
# Verify the exact issue: check if there's proper focus trapping or background element rendering
cat -n src/features/marketing/components/navbar.tsx | tail -20Repository: resq-software/landing
Length of output: 904
Replace the mobile menu with Radix Dialog for proper modal behavior and focus management.
The current implementation relies on inert={!menuOpen} which only disables interaction when the menu is closed. Once open, focus can escape to page elements behind the menu, breaking modal behavior for keyboard users. While body scroll locking and Escape key handling are present, there's no focus trapping to keep focus within the menu or prevent tabbing to background elements.
Use Radix Dialog primitive to provide built-in focus trapping, backdrop overlay, and proper modal semantics. This aligns with the coding guideline requiring Radix UI primitives for components in src/{components,features}/**/*.{ts,tsx}.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/components/navbar.tsx` around lines 303 - 361, Replace
the current manual mobile menu DOM (the div with id="mobile-menu" that uses
inert={!menuOpen}, menuOpen state, and setMenuOpen) with a Radix Dialog
primitive: wrap the menu in Dialog.Root open={menuOpen}
onOpenChange={setMenuOpen}, render the backdrop via Dialog.Overlay and the menu
contents inside Dialog.Content (or Dialog.Portal > Dialog.Content) so Radix
handles focus trapping and Escape/close behavior; remove the inert prop and
existing manual focus logic/escape handlers. Keep the existing mapping over
mobileLinks (including external link handling, target/rel, icon and sr-only
text), the mobile CTA Link (href="/contact") and LocaleSwitcher inside the
Dialog.Content, and import/use Dialog, DialogOverlay, DialogContent, DialogClose
from `@radix-ui/react-dialog` as needed. Ensure styling/classes from the original
container are applied to Dialog.Content and that setMenuOpen(false) is preserved
for link onClick to close the dialog.
| > | ||
| <div className="overflow-x-auto"> | ||
| <table className="w-full min-w-[500px]" aria-label="System architecture layers and technologies"> | ||
| <table className="w-full min-w-125" aria-label="System architecture layers and technologies"> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for min-w-125 definition in Tailwind config or globals.css
# Check if min-w-125 is defined as a custom utility or theme extension
rg -n "min-w-125|125:" src/styles/globals.css
# Also check any tailwind config files
fd -e js -e ts -e mjs "tailwind" --exec cat {} 2>/dev/null | head -100Repository: resq-software/landing
Length of output: 47
🏁 Script executed:
#!/bin/bash
# More comprehensive search for min-w-125 and Tailwind config
# Search for min-w-125 across entire codebase
echo "=== Searching for min-w-125 in all files ==="
rg "min-w-125" .
# Search for all possible Tailwind config file patterns
echo "=== Searching for Tailwind config files ==="
fd "tailwind.config" .
# Check for Tailwind config in package.json
echo "=== Checking package.json for Tailwind config ==="
cat package.json | grep -A 20 "tailwind"
# Look for theme extensions or custom spacing in globals.css
echo "=== Viewing src/styles/globals.css ==="
head -100 src/styles/globals.css
# Search for 500px or similar sizing that min-w-125 might be replacing
echo "=== Searching for 500px references ==="
rg "500px" src/Repository: resq-software/landing
Length of output: 4673
🏁 Script executed:
#!/bin/bash
# View complete globals.css file
cat src/styles/globals.css | tail -100Repository: resq-software/landing
Length of output: 2586
Remove min-w-125 and use an arbitrary Tailwind value or define the custom class.
The class min-w-125 is not defined anywhere in src/styles/globals.css or in any Tailwind configuration file. This class is actively used in both architecture-diagram.tsx and technology.tsx, but since Tailwind's default spacing scale only goes up to 96 (384px), this custom value will not produce any CSS output and the table will not have the intended minimum width constraint.
Either revert to the arbitrary value min-w-[500px] or add min-w-125 to the Tailwind theme configuration in src/styles/globals.css within the @theme inline block.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/sections/architecture-diagram.tsx` at line 95, The
table element in the ArchitectureDiagram component uses a non-existent Tailwind
class "min-w-125" (seen in the table's className "w-full min-w-125"), which
produces no CSS; either replace that token with an arbitrary Tailwind value like
"min-w-[500px]" in the table's className (and likewise in technology.tsx where
used) or add "min-w-125" to your Tailwind theme scale inside the `@theme` inline
block in src/styles/globals.css so the class is generated.
| const SCENARIOS: HeroScenario[] = [ | ||
| { lat: "34.0522° N", lng: "118.2437° W", location: "Los Angeles, CA", meshStatus: "ACTIVE", uptimeBase: 99.8 }, | ||
| { lat: "29.7604° N", lng: "95.3698° W", location: "Houston, TX", meshStatus: "ACTIVE", uptimeBase: 99.6 }, | ||
| { lat: "37.5858° N", lng: "36.9371° E", location: "Kahramanmaraş, Turkey", meshStatus: "ACTIVE", uptimeBase: 99.4 }, | ||
| ]; |
There was a problem hiding this comment.
Defensively bound slideshow index before scenario lookup.
SCENARIOS[videoIndex] can become undefined if slideshow video count and scenario count drift, causing a runtime break in HeroHud when it reads scenario.uptimeBase.
💡 Proposed defensive fix
export function Hero() {
const [videoIndex, setVideoIndex] = useState(0);
+ const currentScenario = SCENARIOS[videoIndex] ?? SCENARIOS[0];
return (
@@
- <HeroVideoSlideshow onIndexChange={setVideoIndex} />
+ <HeroVideoSlideshow onIndexChange={(nextIndex) => setVideoIndex(nextIndex % SCENARIOS.length)} />
@@
- <HeroHud scenario={SCENARIOS[videoIndex]} />
+ <HeroHud scenario={currentScenario} />Also applies to: 30-30, 90-90
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/features/marketing/sections/hero.tsx` around lines 13 - 17,
SCENARIOS[videoIndex] can be undefined causing HeroHud to read properties off
undefined; clamp or defensively resolve the scenario before lookup: compute an
index = Math.max(0, Math.min(videoIndex, SCENARIOS.length - 1)) or select a
fallback scenario when SCENARIOS[videoIndex] is undefined, then pass that safe
scenario into HeroHud and any other places referencing SCENARIOS[videoIndex]
(e.g., where videoIndex is used to read uptimeBase and location), and apply the
same defensive lookup for the other occurrences noted (the other SCENARIOS
access points).
| { lat: "34.0522° N", lng: "118.2437° W", location: "Los Angeles, CA", meshStatus: "ACTIVE", uptimeBase: 99.8 }, | ||
| { lat: "29.7604° N", lng: "95.3698° W", location: "Houston, TX", meshStatus: "ACTIVE", uptimeBase: 99.6 }, | ||
| { lat: "37.5858° N", lng: "36.9371° E", location: "Kahramanmaraş, Turkey", meshStatus: "ACTIVE", uptimeBase: 99.4 }, |
There was a problem hiding this comment.
New scenario text is hardcoded and bypasses localization.
The newly introduced location and meshStatus display values are English literals, which undercuts locale coverage for es/zh/hi/ar.
| export function getPostBySlug(slug: string): Post | null { | ||
| const filePath = path.join(POSTS_DIR, `${slug}.mdx`); | ||
|
|
||
| if (!fs.existsSync(filePath)) return null; |
There was a problem hiding this comment.
Sanitize slug to prevent path traversal.
The slug parameter is used directly in path.join without validation. A malicious slug like ../../../etc/passwd could potentially read files outside POSTS_DIR. While this is likely only called with slugs from generateStaticParams, adding validation provides defense-in-depth.
🛡️ Proposed fix
export function getPostBySlug(slug: string): Post | null {
+ // Prevent path traversal
+ if (slug.includes("..") || slug.includes("/") || slug.includes("\\")) {
+ return null;
+ }
+
const filePath = path.join(POSTS_DIR, `${slug}.mdx`);
if (!fs.existsSync(filePath)) return null;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/content.ts` around lines 59 - 62, getPostBySlug currently uses the
incoming slug directly in path.join which allows path traversal; validate or
sanitize the slug before building filePath (e.g., enforce an allowlist regex
like only letters/numbers/hyphens/underscores or compute resolved =
path.resolve(POSTS_DIR, `${slug}.mdx`) and ensure
resolved.startsWith(path.resolve(POSTS_DIR))). If validation fails, return null
(or throw) and only then read the file; update references to getPostBySlug,
POSTS_DIR, and filePath accordingly.
| // Run i18n locale detection and routing first | ||
| const response = intlMiddleware(request); | ||
|
|
||
| // Apply security headers on top | ||
| const nonce = crypto.randomUUID().replaceAll("-", ""); | ||
| applySecurityHeaders(response, nonce); | ||
| response.headers.set("x-nonce", nonce); | ||
| response.headers.set("x-url", pathname); |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
In Next.js App Router proxy/middleware, when using a nonce-based Content Security Policy, does Next.js need the nonce/CSP to be forwarded on the request for the current render, or is setting them only on the returned NextResponse sufficient?
💡 Result:
In Next.js App Router middleware (or proxy), for nonce-based CSP to work during server-side rendering of the current request, the CSP header containing the nonce must be forwarded on the request via NextResponse.next({ request: { headers: requestHeaders } }) where requestHeaders includes the Content-Security-Policy header with the nonce. Setting the CSP only on the returned NextResponse (response headers) is not sufficient for Next.js to extract and apply the nonce to its injected scripts/styles during rendering. Next.js parses the request's CSP header during SSR to automatically apply the nonce to its own script and link tags (as confirmed in official docs and multiple implementations). The response CSP header is needed for browser enforcement, so both are typically set. Example from official Next.js docs (middleware/proxy): const requestHeaders = new Headers(request.headers); requestHeaders.set('Content-Security-Policy', contentSecurityPolicyHeaderValue); // REQUIRED for render const response = NextResponse.next({ request: { headers: requestHeaders }, }); response.headers.set('Content-Security-Policy', contentSecurityPolicyHeaderValue); // For browser Without the request header, Next.js won't detect the nonce-'nonce-value' in CSP and scripts will lack the nonce attribute or have empty ones, breaking under strict CSP.
Citations:
- 1: https://nextjs.org/docs/app/guides/content-security-policy
- 2: https://nextjs.org/docs/14/app/building-your-application/configuring/content-security-policy
- 3: https://justappsec.com/guides/nextjs-csp-configuration
- 4: https://centralcsp.com/articles/how-to-setup-nonce-with-nextjs
- 5: https://stackoverflow.com/questions/76353091/how-to-add-nonce-to-inline-styles-and-scripts-in-next-js/76567353
Forward the nonce and CSP header in the request, not just the response.
For nonce-based CSP in Next.js App Router, the CSP header containing the nonce must be set on the forwarded request via NextResponse.next({ request: { headers: requestHeaders } }). Setting these headers only on the response is insufficient—Next.js parses the request's CSP header during SSR to inject the nonce into its own scripts and styles. Without the nonce in the request headers, Next.js will not apply the nonce attribute, breaking the CSP policy.
Update the code to forward the nonce and CSP on the request headers before calling intlMiddleware, or ensure applySecurityHeaders sets them on both the request and response appropriately.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/proxy.ts` around lines 92 - 99, The code sets the nonce and CSP only on
the response, but Next.js requires the CSP header (with nonce) on the forwarded
request so SSR can inject nonce attributes; update the flow around
intlMiddleware and applySecurityHeaders to add the nonce and CSP into the
outgoing request headers (e.g., build or mutate requestHeaders and call
NextResponse.next({ request: { headers: requestHeaders } })) rather than only
setting them on the response, or ensure applySecurityHeaders writes both request
and response headers; modify where nonce is generated (keep using
crypto.randomUUID().replaceAll("-", "")) and ensure the same nonce is applied to
the CSP header and forwarded on the request before
intlMiddleware/NextResponse.next is invoked.
| const envBaseUrl = | ||
| process?.env?.['VITE_BASE_URL'] || | ||
| process?.env?.['NEXT_PUBLIC_BASE_URL'] || | ||
| process?.env?.['BASE_URL']; | ||
| process?.env?.VITE_BASE_URL || | ||
| process?.env?.NEXT_PUBLIC_BASE_URL || | ||
| process?.env?.BASE_URL; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
# Check if src/env.ts exists and examine its structure
fd -t f "env\.(ts|js)" src/ | head -20Repository: resq-software/landing
Length of output: 75
🏁 Script executed:
# Get the full content of src/utils/helpers.ts to understand context
wc -l src/utils/helpers.tsRepository: resq-software/landing
Length of output: 89
🏁 Script executed:
# Examine the env.ts file if it exists
cat src/env.ts 2>/dev/null || echo "File not found"Repository: resq-software/landing
Length of output: 2310
🏁 Script executed:
# Check the full content of src/utils/helpers.ts around lines 65-78
sed -n '1,85p' src/utils/helpers.ts | cat -nRepository: resq-software/landing
Length of output: 3778
🏁 Script executed:
# Search for usage of VITE_BASE_URL, BASE_URL, NEXT_PUBLIC_BASE_URL in codebase
rg "VITE_BASE_URL|BASE_URL|NEXT_PUBLIC_BASE_URL" --type ts --type tsx --type js --type jsx -nRepository: resq-software/landing
Length of output: 92
🏁 Script executed:
# Check if helpers.ts imports from src/env.ts
grep -n "import.*env" src/utils/helpers.tsRepository: resq-software/landing
Length of output: 47
🏁 Script executed:
# Search for how getURL is being used in the codebase
rg "getURL" --type ts --type tsx --type js --type jsx -B 2 -A 2Repository: resq-software/landing
Length of output: 92
🏁 Script executed:
# Search for usage of VITE_BASE_URL, BASE_URL, NEXT_PUBLIC_BASE_URL in codebase without type restriction
rg "VITE_BASE_URL|BASE_URL|NEXT_PUBLIC_BASE_URL" -nRepository: resq-software/landing
Length of output: 634
🏁 Script executed:
# Check if helpers.ts imports from src/env.ts
cat src/utils/helpers.ts | grep -i "import"Repository: resq-software/landing
Length of output: 99
🏁 Script executed:
# Search for how getURL is being used
rg "getURL" -B 2 -A 2Repository: resq-software/landing
Length of output: 4030
Environment variables must be validated through src/env.ts, not accessed directly from process.env.
Lines 76–78 access process?.env directly, which violates the project guideline requiring all environment variables to be validated via @t3-oss/env-nextjs. Additionally, optional chaining on an undeclared process identifier can still throw a ReferenceError in non-Node.js environments before the optional chain takes effect.
Add VITE_BASE_URL, NEXT_PUBLIC_BASE_URL, and BASE_URL to the schema in src/env.ts, then import and use the validated env export instead:
Suggested fix
-import { logger } from "./logger";
+import { logger } from "./logger";
+import { env } from "@/env";- const envBaseUrl =
- process?.env?.VITE_BASE_URL ||
- process?.env?.NEXT_PUBLIC_BASE_URL ||
- process?.env?.BASE_URL;
+ const envBaseUrl =
+ env.NEXT_PUBLIC_BASE_URL || env.NEXT_PUBLIC_APP_URL;Also update src/env.ts to include these variables in the clientEnvSchema.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/utils/helpers.ts` around lines 75 - 78, The code reads VITE_BASE_URL /
NEXT_PUBLIC_BASE_URL / BASE_URL directly from process.env (and uses optional
chaining on process) which violates the env validation rule; add VITE_BASE_URL,
NEXT_PUBLIC_BASE_URL, and BASE_URL to the clientEnvSchema in src/env.ts (so they
are validated and exposed on the client as needed), export the validated env,
then replace the direct process?.env access in the helper with the imported
validated env (e.g., use env.VITE_BASE_URL || env.NEXT_PUBLIC_BASE_URL ||
env.BASE_URL) and remove the optional chaining on process.
Summary by CodeRabbit
New Features
Documentation
Improvements