Skip to content

Conversation

@maamokun
Copy link
Collaborator

@maamokun maamokun commented Oct 27, 2025

Summary by CodeRabbit

  • New Features

    • Interactive cursor options and a Settings panel (click-spark, splash, custom, target).
    • Many new visual components: CircularText, Typewriter/Marquee variants, FluidGlass, VRM viewer, SpotlightCard, BubbleMenu, FaultyTerminal, Cursor toys, ClickSpark, SplashCursor, TargetCursor, Click-spark canvas, Card/Button/Spinner primitives, Consent manager.
    • New locale-aware pages and layouts, redirects and image loader.
  • Improvements

    • Token-based theming, expanded design tokens, richer animations.
    • Reworked homepage and reorganised translations; simplified image loading in dev/prod.
  • Removals

    • Multiple legacy pages and old UI components removed.

✏️ Tip: You can customize this high-level summary in your review settings.

@vercel
Copy link

vercel bot commented Oct 27, 2025

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

Project Deployment Review Updated (UTC)
mikn-dev Ready Ready Preview, Comment Jan 8, 2026 4:25pm

@coderabbitai
Copy link

coderabbitai bot commented Oct 27, 2025

Important

Review skipped

Auto reviews are disabled on this repository.

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

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

Walkthrough

Replaces Cloudflare/Open‑Next infra and many legacy UI pieces with a modern Next.js frontend: adds interactive UI primitives (cursor toys, 3D/VRM, R3F, GSAP/Framer Motion components), utilities, relocated pages/layouts and i18n restructuring; removes old deploy manifests, legacy image loader and many legacy components.

Changes

Cohort / File(s) Change Summary
Build & Tooling
package.json, next.config.mjs, tsconfig.json, biome.json, postcss.config.mjs, renovate.json, bunfig.toml
Modernised scripts and deps, enabled Next reactCompiler, TS JSX/runtime and strict flag changes, replaced Biome schema, minor formatting edits.
Deployment / Infra Removed
wrangler.jsonc, open-next.config.ts, .dockerignore, .idea/copilot.data.migration.*
Removed Cloudflare/Open‑Next deployment manifests and wiped .dockerignore; added IDE migration state files.
Global Styling & Theming
src/app/globals.css, tailwind.config.ts, components.json, postcss.config.mjs
Replaced DaisyUI with tokenised CSS variables, added Tailwind plugins and ShadCN config; formatting tweaks.
Routing / Redirects / Proxy
src/public/_redirects, src/proxy.ts
Added static redirect rules (root → /en, catch‑all → /en/:splat); simplified middleware matcher to a single negative‑lookahead regex.
Image Loader
imgLoader.ts (new), src/imgLoader.ts (removed)
Replaced Cloudflare loader with env‑driven optimizer loader; removed old Cloudflare-specific loader.
i18n & Messages
src/i18n/request.ts, src/i18n/routing.ts, src/messages/en.json, src/messages/ja.json
Hardcoded locale load to "en" with local message import; reorganised translation keys and restructured home/layout content.
App Layouts & Pages
src/app/layout.tsx, src/app/[locale]/layout.tsx, src/app/[locale]/template.tsx, src/app/not-found.tsx, src/app/[locale]/page.tsx
Reworked root and locale layouts/providers (NextIntlClientProvider, CursorToys, ConsentManager), swapped fonts and changed layout composition/providers.
Removed Pages
src/app/[locale]/{about,contact,cost,solutions,tech}/page.tsx
Deleted multiple locale-specific page components and their default exports.
Legacy UI Removal
src/components/nUI/*, src/components/fancy/{NumberTicker,code-comp,icon-cloud,marqee,typewriter}.tsx, src/components/codecomp-v3v4.tsx, src/imgLoader.ts
Removed legacy header/footer/menu-toggle and multiple “fancy” UI components and exports.
Animation / UI Primitives
src/components/animate-ui/**, src/components/ui/*, src/components/ui/shadcn-io/spinner/index.tsx
Added AnimateIcon system, motion-aware Slot, animated Radix primitives, CVA-based Button variants and multi‑variant Spinner.
Cursor Effects System
src/contexts/CursorToysContext.tsx, src/components/{CursorToys,GlobalCursorToys,SettingsController,consent-manager}.tsx, src/components/{ClickSpark,CustomCursor,SplashCursor,TargetCursor}.tsx, src/interfaces/CursorToys.ts
New CursorToys context, four cursor-toy components, settings UI and consent wrapper.
3D / Visual Components
src/components/{vrm.tsx,FluidGlass.tsx}, src/components/BubbleMenu.tsx, src/components/SpotlightCard.tsx
Added VRM with animation/look‑at control, FluidGlass (R3F FBO glass), BubbleMenu (GSAP) and SpotlightCard (cursor spotlight).
Text / Marquee / Ticker
src/components/{CircularText,SplitText}.tsx, src/components/fancy/blocks/{marquee-along-svg-path,simple-marquee}.tsx, src/components/fancy/text/basic-number-ticker.tsx
Added CircularText, SplitText (GSAP), marquee variants and a new NumberTicker (imperative API).
Utilities & Hooks
src/lib/utils.ts, src/lib/get-strict-context.tsx, src/hooks/{use-controlled-state,use-is-in-view}.tsx, src/components/cn.ts
Added cn (clsx + twMerge), strict-context factory, controlled-state and in-view hooks; small formatting changes.
New UI: Mikn Header/Footer & Others
src/components/mikn/{Header,Footer}.tsx, src/components/ui/button.tsx, src/components/ClickSpark.tsx, src/components/SpotlightCard.tsx
Added Mikn Header/Footer, new UI Button, ClickSpark and SpotlightCard components.
New Pages / Layouts
src/app/[locale]/(pages)/template.tsx, src/app/[locale]/(pages)/contact/page.tsx, src/app/api/[...slugs]/route.ts
Added new PagesLayout, locale contact page and a minimal Elysia API route.
Misc / Docs / Config
AGENTS.md, opencode.json, .idea/*
Added agent guide, opencode config and IDE migration state files; misc formatting updates.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant User
participant PagesLayout
participant SettingsController
participant CursorToysContext
participant CursorToyComponent
User->>PagesLayout: open page
PagesLayout->>SettingsController: render settings UI
User->>SettingsController: toggle toy (e.g., "spark")
SettingsController->>CursorToysContext: setSelectedToy("spark")
CursorToysContext-->>SettingsController: persist state
PagesLayout->>CursorToysContext: read selectedToy
CursorToysContext->>CursorToyComponent: provide selectedToy
CursorToyComponent->>User: render ClickSpark / CustomCursor / Splash / TargetCursor

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

  • fix: PostCSS config #18 — Overlaps PostCSS and Tailwind integration changes; touches similar tooling and styling files.
  • feat: Tailwind v4 #16 — Related changes to globals.css and DaisyUI removal; likely connected to theming refactor.

Poem

I hopped into the repo bright, 🐇
New sparks and cursors danced in light,
Circles spun and models bowed,
Plugins, shaders — the burrow’s proud,
A rabbit claps for code tonight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 1.96% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Refactor' is vague and does not convey meaningful information about the specific changes made in this substantial pull request. Provide a more descriptive title that highlights the primary change, such as 'Migrate from Next.js/Cloudflare to React Three Fiber and animate-ui components' or 'Restructure frontend with new cursor toys, animations, and component library'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@gemini-code-assist
Copy link

Summary of Changes

Hello @maamokun, 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 undertakes a comprehensive refactoring of the project, focusing on modernizing the frontend architecture and optimizing the deployment process. Key aspects include updating critical dependencies, transitioning to a new UI component library (shadcn/ui) with a custom Tailwind CSS theme, and simplifying page structures. The changes also reflect a strategic shift in the deployment approach, moving towards static exports and away from previous Cloudflare Workers/OpenNext configurations, alongside general project cleanup.

Highlights

  • Dependency Updates: Several core dependencies in package.json have been updated, including next to ^16.0.0, react to ^19.2.0, and @biomejs/biome to ^2.3.1. New dependencies like @radix-ui/react-slot, class-variance-authority, lucide-react, tailwindcss-animate, and tw-animate-css have been added, while @opennextjs/cloudflare and @opennextjs/aws have been removed.
  • Biome Configuration Overhaul: The biome.json configuration has been significantly refactored, updating the schema version, changing formatter settings (e.g., indentStyle from tab to space, indentWidth from 4 to 2), and adding new rules and domains for linting (e.g., suspicious.noUnknownAtRules: "off", domains.next: "recommended", domains.react: "recommended").
  • Next.js and Deployment Strategy Shift: The next.config.mjs file now includes output: "export", indicating a move towards static export. The open-next.config.ts file has been removed, along with wrangler.jsonc and bunfig.toml, suggesting a shift away from Cloudflare Workers/OpenNext deployment.
  • UI Component and Styling Refactor: Multiple page files (src/app/[locale]/about/page.tsx, src/app/[locale]/contact/page.tsx, etc.) have been removed or drastically simplified. New consent management files (src/app/consent-manager.client.tsx, src/app/consent-manager.tsx) and a components.json for shadcn/ui have been added. The src/app/globals.css has been completely rewritten to remove daisyui and introduce a new custom theme with CSS variables and tailwindcss-animate.
  • Project Configuration Cleanup: The .dockerignore and bunfig.toml files have been removed, streamlining project configuration. New .idea/copilot.data.migration.ask.xml and .idea/copilot.data.migration.ask2agent.xml files have been added, likely for IDE integration with Copilot.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@gemini-code-assist
Copy link

Warning

Gemini encountered an error creating the review. You can try again by commenting /gemini review.

@cloudflare-workers-and-pages
Copy link

cloudflare-workers-and-pages bot commented Oct 27, 2025

Deploying mikn-dev with  Cloudflare Pages  Cloudflare Pages

Latest commit: 4dba606
Status:⚡️  Build in progress...

View logs

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 22

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/components/fancy/icon-cloud.tsx (1)

1-4: Remove renderToString import from client component and use a browser-compatible alternative.

renderToString is a server-only API in React 19 and must not be imported in client components marked with "use client". Although it may execute in the browser, doing so violates React 19's architecture, bloats the bundle, and is explicitly discouraged. Convert SVG React elements to strings using createRoot with a temporary container, or adopt the new static/prerender APIs instead. Alternatively, move icon preprocessing to a server component if feasible.

🧹 Nitpick comments (20)
src/components/fancy/icon-cloud.tsx (2)

285-293: Improve accessibility of the numbered circles fallback.

The fallback numbered circles (lines 285–293) lack semantic meaning for screen readers. Since the canvas already has role="img" and aria-label, consider:

  • Removing the fallback circles and rendering nothing whilst icons load.
  • Or, rendering a more descriptive fallback with better contrast and size.

Current approach is a minor accessibility concern because the circles are visual-only.


308-308: Review the dependency array for potential excessive re-renders.

The dependency array includes iconPositions, isDragging, mousePos, and targetRotation objects. Since these are updated on every render or animation frame, the effect may re-run more often than intended. Consider using a stable reference or narrowing dependencies:

- }, [icons, images, iconPositions, isDragging, mousePos, targetRotation]);
+ }, [icons, images]);

The animation loop uses rotationRef and other refs directly, so explicit dependency on state changes is unnecessary—the animation frame itself will capture updates via ref access.

Confirm that the animation remains smooth and responsive after reducing the dependency array.

src/messages/ja.json (1)

19-21: Polish JP localisation (typo + formalise cookie consent + correct “Legal” label).

  • Fix duplicated の (Line 19).
  • Replace “法的条約” with a more accurate “法的情報” (Line 40).
  • Make consent copy formal and compliant; “いいね!” → “同意する” (Lines 51–52).

If your ConsentManager supports “Reject”/“Manage preferences”, consider adding keys for those buttons too.

-    "SimpleNCheap": "シンプルさを追求し生まれた究極ののコストパフォマンス",
+    "SimpleNCheap": "シンプルさを追求して生まれた究極のコストパフォーマンス",

-    "legal": "法的条約",
+    "legal": "法的情報",

-    "cookieConsent": "そこの君!このサイトはCookieを使ってるぞ!!いいよね?",
-    "accept": "いいね!"
+    "cookieConsent": "当サイトでは、機能の提供および分析のためにクッキーを使用します。詳細はプライバシーポリシーをご確認ください。クッキーの使用に同意しますか?",
+    "accept": "同意する"

Also applies to: 40-42, 51-52

src/components/nUI/hooks.tsx (1)

5-12: Simplify hook: return useMedia directly; remove redundant state/effect.

Reduces re-renders and removes the need for the eslint-disable.

-export const useIsWide = () => {
-  const [isWide, setIsWide] = useState(false);
-
-  const _isWide = useMedia("(min-width: 780px)", false);
-
-  useEffect(() => {
-    setIsWide(_isWide);
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [_isWide]);
-
-  return isWide;
-};
+export const useIsWide = () => {
+  return useMedia("(min-width: 780px)", false);
+};

Additionally, remove now‑unused React imports at the top:

-import { useEffect, useState } from "react";
+// no React hooks needed here

Also applies to: 14-14

src/messages/en.json (1)

51-52: Use neutral, explicit cookie consent copy.

Improve clarity and align with consent UX.

-    "cookieConsent": "We use cookies for analytics and making stuff work. By clicking 'mmmmmmm cookies 🍪', you agree to our use of cookies.",
-    "accept": "mmmmmmm cookies 🍪"
+    "cookieConsent": "We use cookies to enable site functionality and for analytics. See our Privacy Policy. Do you consent to the use of cookies?",
+    "accept": "Accept"
src/components/fancy/marqee.tsx (2)

56-71: Guard repeat and respect reduced motion

Prevent RangeError for non-integers/negatives and add a reduced‑motion fallback for accessibility.

-      {Array(repeat)
-        .fill(0)
+      {Array.from({ length: Math.max(1, Math.floor(repeat)) })
         .map((_, i) => (
           <div
             key={i}
-            className={cn("flex shrink-0 justify-around gap-(--gap)", {
+            className={cn("flex shrink-0 justify-around gap-(--gap) motion-reduce:animate-none", {
               "animate-marquee flex-row": !vertical,
               "animate-marquee-vertical flex-col": vertical,
               "group-hover:[animation-play-state:paused]": pauseOnHover,
               "[animation-direction:reverse]": reverse,
             })}
           >

1-1: Spelling and import duplication

File name is “marqee.tsx”. Consider renaming to “marquee.tsx” and exporting from an index to avoid breaking imports. Also note two cn utilities exist (src/components/cn.ts and src/lib/utils.ts). Consolidate to a single source to avoid drift.

src/components/nUI/Header.tsx (5)

276-313: Use next/link for internal desktop navigation

<a> causes full page reloads and bypasses prefetch. Prefer <Link> for internal routes.

-                <motion.a
-                  key={item.name}
-                  href={item.href}
+                <motion.div
+                  key={item.name}
                   className="group relative text-sm font-semibold leading-6 text-on-background"
                   initial="rest"
                   whileHover="hover"
                   animate="rest"
                 >
-                  {item.name}
+                  <Link href={item.href} className="relative">
+                    {item.name}
+                  </Link>
                   {isCurrent ? (
...
-                </motion.a>
+                </motion.div>

97-103: Remove unused prop index from MobileMenuItemProps and call sites

index isn’t used inside MobileMenuItem. Drop it to keep the API tight.

-interface MobileMenuItemProps {
+interface MobileMenuItemProps {
   name: string;
   href: string;
-  index: number;
   isCurrent: boolean;
   color: string;
 }
...
-                  <MobileMenuItem
+                  <MobileMenuItem
                     key={item.name}
                     name={item.name}
                     href={item.href}
-                    index={index}
                     color={_color}
                     isCurrent={isCurrent}
                   />

Also applies to: 226-241


206-211: Optimise the logo image

Use next/image for better performance and add width/height/alt. If sticking to <img>, add loading="lazy" and decoding="async".


175-175: Avoid magic header heights; derive from ref

headerHeight = 88 and hide: { top: -88 } can drift from actual layout (h-12 / lg:h-16). Measure offsetHeight via a ref and compute dynamically.

Also applies to: 87-95


179-186: Throttle scroll updates to reduce re-render churn

Scroll fires very frequently; batching/throttling (e.g., requestAnimationFrame) can smooth performance.

src/app/[locale]/template.tsx (1)

2-5: Remove unused imports/vars and make analytics configurable

  • useTranslations, useRouter, usePathname, and their variables are unused.
  • Move the Swetrix key and API URL to env vars and optionally gate initialisation on consent (given the new ConsentManager in this PR).
  • The extra <div> wrapper isn’t necessary; return children directly.
-"use client";
-import { useTranslations } from "next-intl";
+"use client";
 import { useSwetrix } from "@swetrix/nextjs";
-import { useRouter, usePathname } from "next/navigation";
-import { ReactNode } from "react";
+import { ReactNode, useEffect } from "react";
 
 export default function PagesLayout({ children }: { children: ReactNode }) {
-  const router = useRouter();
-  const pathname = usePathname();
-  const t = useTranslations("nav");
-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+  // TODO: read consent from context and guard initialisation
+  useSwetrix(process.env.NEXT_PUBLIC_SWETRIX_KEY!, {
+    apiURL: process.env.NEXT_PUBLIC_SWETRIX_API_URL,
+  });
 
-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return <>{children}</>;
 }

Also applies to: 8-11, 13-17

src/app/layout.tsx (1)

11-15: Remove unnecessary fragment wrapper.

The fragment <>{children}</> on line 13 is redundant when wrapping a single expression.

Apply this diff:

   return (
     <ConsentManager>
-      <>{children}</>
+      {children}
     </ConsentManager>
   );
src/app/globals.css (3)

6-6: Custom dark variant selector may be overly broad; prefer ancestor-scoped form.

Use an ancestor-aware selector to avoid matching unintended contexts and align with common patterns:

-@custom-variant dark (&:is(.dark *));
+@custom-variant dark (&:where(.dark &));

This scopes “dark:” styles to elements under a .dark ancestor with better specificity behaviour. If you’re already using Tailwind’s built-in dark variant, consider removing this to avoid duplicate variants.


68-126: .dark variables duplicate :root; remove or override only differences.

The .dark block repeats all tokens with identical values as :root. This adds maintenance overhead without effect. Either delete the block or keep only actual overrides (e.g., background, foreground, accent).


181-188: Global base utilities: confirm outline colour utility is generated.

@apply outline-ring/50 relies on “ring” being a named colour token in @theme. If Tailwind doesn’t emit outline colour utilities for custom tokens in your setup, this may no-op. Verify it compiles to outline-color. If not, consider:

  • Using ring utilities instead (focus-visible:ring-... already present on Button), or
  • Switching to an explicit colour (e.g., outline-[color:oklch(...)]) for guarantees.
src/components/ui/button.tsx (1)

7-35: Variants look solid; consider exposing a data attribute for state styling (optional).

If you plan to style loading/pending states (e.g., with React Actions), exposing data-state attributes can help:

  • data-pending, data-success, data-error, etc., for Tailwind selectors (data-[pending]:opacity-50).
src/components/vrm.tsx (2)

48-60: Avoid setting static transforms every frame.

Position/scale/rotation and constant expression can be set once (e.g., useEffect) instead of on every animation frame. This reduces per‑frame work and avoids fighting external transforms.

Example:

-  useFrame(({ clock }, delta) => {
-    if (vrm) {
-      vrm.scene.position.set(0, -4.2, 0);
-      vrm.scene.scale.set(6.5, 5, 5);
-      vrm.scene.rotation.y = Math.PI;
-      vrm.expressionManager?.setValue("neutral", 1);
-      vrm.update(delta);
-    }
-    if (mixer) {
-      mixer.update(delta);
-    }
-  });
+  useEffect(() => {
+    if (!vrm) return;
+    vrm.scene.position.set(0, -4.2, 0);
+    vrm.scene.scale.set(6.5, 5, 5);
+    vrm.scene.rotation.y = Math.PI;
+    vrm.expressionManager?.setValue("neutral", 1);
+  }, [vrm]);
+
+  useFrame((_, delta) => {
+    vrm?.update(delta);
+    mixer?.update(delta);
+  });

94-104: Guard against zero/invalid weight totals in the animation picker.

Small resilience tweak: return early if total <= 0 to avoid NaNs when all weights are zero.

     function pickAnimation() {
-      const total = animations.reduce((sum, anim) => sum + anim.percentage, 0);
+      const total = animations.reduce((sum, anim) => sum + anim.percentage, 0);
+      if (total <= 0) return animations[0];
       const rand = Math.random() * total;
       let acc = 0;
       for (const anim of animations) {
         acc += anim.percentage;
         if (rand < acc) return anim;
       }
       return animations[0]; // fallback
     }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8fdb4a2 and 7abd5fc.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (47)
  • .dockerignore (0 hunks)
  • .idea/copilot.data.migration.ask.xml (1 hunks)
  • .idea/copilot.data.migration.ask2agent.xml (1 hunks)
  • biome.json (1 hunks)
  • bunfig.toml (0 hunks)
  • components.json (1 hunks)
  • next.config.mjs (1 hunks)
  • open-next.config.ts (0 hunks)
  • package.json (1 hunks)
  • postcss.config.mjs (1 hunks)
  • renovate.json (1 hunks)
  • src/app/[locale]/about/page.tsx (0 hunks)
  • src/app/[locale]/contact/page.tsx (0 hunks)
  • src/app/[locale]/cost/page.tsx (0 hunks)
  • src/app/[locale]/layout.tsx (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/solutions/page.tsx (0 hunks)
  • src/app/[locale]/tech/page.tsx (0 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/consent-manager.client.tsx (1 hunks)
  • src/app/consent-manager.tsx (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/app/not-found.tsx (1 hunks)
  • src/components/cn.ts (1 hunks)
  • src/components/codecomp-v3v4.tsx (0 hunks)
  • src/components/fancy/NumberTicker.tsx (1 hunks)
  • src/components/fancy/code-comp.tsx (1 hunks)
  • src/components/fancy/icon-cloud.tsx (1 hunks)
  • src/components/fancy/marqee.tsx (1 hunks)
  • src/components/fancy/typewriter.tsx (1 hunks)
  • src/components/nUI/Footer.tsx (1 hunks)
  • src/components/nUI/Header.tsx (1 hunks)
  • src/components/nUI/MenuToggle.tsx (1 hunks)
  • src/components/nUI/hooks.tsx (1 hunks)
  • src/components/ui/button.tsx (1 hunks)
  • src/components/vrm.tsx (1 hunks)
  • src/i18n/request.ts (1 hunks)
  • src/i18n/routing.ts (1 hunks)
  • src/imgLoader.ts (1 hunks)
  • src/lib/utils.ts (1 hunks)
  • src/messages/en.json (1 hunks)
  • src/messages/ja.json (1 hunks)
  • src/middleware.ts (0 hunks)
  • tailwind.config.ts (1 hunks)
  • tsconfig.json (1 hunks)
  • wrangler.jsonc (0 hunks)
💤 Files with no reviewable changes (11)
  • src/components/codecomp-v3v4.tsx
  • src/app/[locale]/tech/page.tsx
  • src/app/[locale]/cost/page.tsx
  • src/app/[locale]/contact/page.tsx
  • src/middleware.ts
  • bunfig.toml
  • src/app/[locale]/about/page.tsx
  • open-next.config.ts
  • src/app/[locale]/solutions/page.tsx
  • wrangler.jsonc
  • .dockerignore
🧰 Additional context used
🧬 Code graph analysis (9)
src/i18n/request.ts (1)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/nUI/Header.tsx (2)
src/components/nUI/hooks.tsx (1)
  • useIsWide (4-15)
src/components/nUI/MenuToggle.tsx (1)
  • MenuToggle (18-43)
src/app/layout.tsx (2)
src/app/[locale]/layout.tsx (1)
  • metadata (13-19)
src/app/consent-manager.tsx (1)
  • ConsentManager (8-20)
src/app/[locale]/layout.tsx (1)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/fancy/marqee.tsx (2)
src/components/cn.ts (1)
  • cn (4-6)
src/lib/utils.ts (1)
  • cn (4-6)
src/components/fancy/typewriter.tsx (2)
src/components/cn.ts (1)
  • cn (4-6)
src/lib/utils.ts (1)
  • cn (4-6)
src/app/[locale]/page.tsx (1)
src/components/ui/button.tsx (1)
  • Button (57-57)
src/components/fancy/NumberTicker.tsx (1)
src/components/cn.ts (1)
  • cn (4-6)
src/components/ui/button.tsx (1)
src/lib/utils.ts (1)
  • cn (4-6)
🪛 ast-grep (0.39.6)
src/components/fancy/code-comp.tsx

[warning] 65-65: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🔇 Additional comments (11)
renovate.json (1)

2-3: Formatting consistency update — no functional changes.

The indentation adjustment maintains the same configuration behaviour and schema validation. No concerns with this formatting-only change.

src/imgLoader.ts (1)

1-22: Good formatting refactoring with sound logic intact.

The restructured parameter destructuring and improved whitespace maintain identical runtime behaviour whilst enhancing readability. The image loader utility is correctly implemented for Cloudflare CDN integration, with appropriate early return for development mode and optional quality parameter handling.

.idea/copilot.data.migration.ask2agent.xml (1)

1-6: Inconsistency with AI-generated summary flagged.

The AI-generated summary claims there is a typo "COMPLTED" in line 4 of this file, but the actual code shows the correct spelling "COMPLETED". This discrepancy should be noted.

src/components/fancy/icon-cloud.tsx (1)

49-49: The review comment is incorrect; type handling is already sound.

The code properly manages the React.ReactNode[] | string[] union through explicit runtime type checks at line 59 (if (images) { ... as string } else { ... as React.ReactElement }). Type assertions align precisely with control flow, and rendering logic includes appropriate guards at line 225 before canvas operations.

Additionally, the suggested fix introduces a semantic change: replacing || with ?? would alter fallback behaviour for empty arrays, which is unintended. The current implementation is type-safe and requires no modification.

Likely an incorrect or invalid review comment.

src/i18n/routing.ts (1)

5-9: Formatting only — looks good.

No semantic changes; locales and defaultLocale remain intact.

Also applies to: 15-15

tsconfig.json (2)

14-14: Correct JSX runtime for React 19.

The change from "preserve" to "react-jsx" aligns with React 19's requirement for the modern JSX transform. This enables the automatic JSX runtime without needing to import React in every file.


34-34: Good addition for Next.js dev types.

Including .next/dev/types/**/*.ts ensures TypeScript picks up Next.js-generated type definitions during development, improving type safety.

src/app/consent-manager.tsx (1)

1-20: Well-structured consent management implementation.

The component correctly wraps children with the consent provider and includes the necessary UI components (banner and dialogue). The offline mode is appropriate for a privacy-first approach, as it avoids external calls whilst managing consent locally.

src/app/consent-manager.client.tsx (1)

22-55: LGTM!

The client-side consent manager wrapper is well-structured and documented. The empty scripts and callbacks placeholders with inline examples provide clear guidance for future integrations.

src/app/globals.css (1)

3-4: Verify Tailwind plugin/import compatibility with v4.

Ensure tailwindcss-animate and tw-animate-css are v4-compatible (Oxide engine, new directives). Mismatched versions can silently drop utilities or keyframes.
Would you like a quick check script to list produced utilities at build time?

src/components/vrm.tsx (1)

4-6: Revert to investigate the @ts-ignore separately; the stated rationale does not apply to this codebase.

The review's premise is incorrect. Your TypeScript configuration does not have verbatimModuleSyntax enabled (it defaults to false), and esModuleInterop: true with moduleResolution: "bundler" already permits mixed value and type imports from the same module. The codebase uses import type patterns successfully elsewhere without .js extensions, which is the established convention here.

The @ts-ignore on line 4 may still warrant investigation—it could indicate an untyped export from three/examples/jsm/loaders/GLTFLoader—but the recommendation to add .js extensions and split the import contradicts both your TypeScript configuration and the codebase's import patterns. If there is a genuine typing issue with the GLTFLoader import, verify it separately rather than follow the suggested changes.

Likely an incorrect or invalid review comment.

Comment on lines +1 to +6
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AskMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project> No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

IDE metadata should not be committed to version control.

Files under .idea/ are JetBrains IDE configuration and state files that are typically excluded from repositories. These can cause merge conflicts, bloat the repository history, and introduce unnecessary noise. Ensure that .idea/ is properly listed in .gitignore and remove this file from the commit.

🤖 Prompt for AI Agents
In .idea/copilot.data.migration.ask.xml (lines 1-6) you committed IDE metadata;
remove this file from the repo and add .idea/ (or at least this
filename/pattern) to .gitignore. Specifically: delete the file from the commit
(git rm --cached .idea/copilot.data.migration.ask.xml), add an entry for .idea/
to .gitignore (or confirm it exists), commit the .gitignore change and the
removal, and push; optionally run git rm --cached -r .idea/ if other .idea files
were committed.

Comment on lines +1 to +6
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Ask2AgentMigrationStateService">
<option name="migrationStatus" value="COMPLETED" />
</component>
</project> No newline at end of file
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

IDE metadata should not be committed to version control.

Files under .idea/ are JetBrains IDE configuration and state files that are typically excluded from repositories. These can cause merge conflicts, bloat the repository history, and introduce unnecessary noise. Ensure that .idea/ is properly listed in .gitignore and remove this file from the commit.

🤖 Prompt for AI Agents
In .idea/copilot.data.migration.ask2agent.xml lines 1-6: this is IDE metadata
that should not be committed; remove this file from the repository and add
.idea/ (or at minimum this filename) to .gitignore so it won’t be tracked in
future commits; if the file is already in git history, remove it from the index
with git rm --cached and commit the removal, then push the change.

Comment on lines +8 to +11
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify files.includes negation support

Confirm Biome v2.2 supports negation with ! in files.includes. If not, switch to files.ignore for .next, dist, build, and node_modules.


🌐 Web query:

Does Biome 2.2 support "!" negation in files.includes? What’s the recommended way to exclude .next, dist, build, and node_modules in biome.json?

💡 Result:

Short answer: Yes — Biome supports negated patterns starting with "!" in files.includes (but you must precede them with a "**" include). For generated/output folders prefer the force-ignore "!!" syntax so they are completely excluded from indexing. Node modules are ignored by default. [1][2][3]

Recommended biome.json snippet (concise):

{
"files": {
"includes": [
"",
"!
/.next",
"!!/dist",
"!!
/build",
"!**/node_modules"
]
}
}

Notes:

  • The leading "**" is required before negations in files.includes. [1][2]
  • Use "!!" for dist/build to prevent indexing (recommended for output folders). [2][3]
  • node_modules is ignored by Biome regardless of files.includes, but you can still add an explicit "!**/node_modules" for clarity. [1]

Update files.includes patterns to use proper glob syntax and force-ignore for build outputs

The negation patterns lack the required path prefix. Update to:

"includes": ["**", "!**/node_modules", "!**/.next", "!!**/dist", "!!**/build"]

Use !! (force-ignore) for dist and build to prevent indexing of generated output folders. Add **/ before each negation pattern for correct glob matching.

🤖 Prompt for AI Agents
In biome.json around lines 8 to 11, the glob negation patterns under
"files.includes" are incorrect and should include path prefixes and force-ignore
for build outputs; update the array so each negation uses the "**/" prefix
(e.g., "!**/node_modules", "!**/.next") and use double-negation force-ignore for
generated folders ("!!**/dist", "!!**/build") to ensure proper glob matching and
prevent indexing of generated output.

Comment on lines 1 to 22
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "neutral",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that referenced paths exist and align with project structure.

The ShadCN UI configuration references specific Tailwind and CSS paths, as well as module aliases. These should be verified to ensure they exist and are correctly configured in the TypeScript/Next.js setup.


🏁 Script executed:

#!/bin/bash
# Description: Verify that ShadCN configuration paths exist and aliases are correctly defined.

# Check if referenced Tailwind config exists
echo "=== Checking tailwind.config.ts ==="
if [ -f "tailwind.config.ts" ]; then
  echo "✓ tailwind.config.ts exists"
  head -20 tailwind.config.ts
else
  echo "✗ tailwind.config.ts not found"
fi

# Check if referenced CSS file exists
echo ""
echo "=== Checking src/app/globals.css ==="
if [ -f "src/app/globals.css" ]; then
  echo "✓ src/app/globals.css exists"
else
  echo "✗ src/app/globals.css not found"
fi

# Check if alias directories exist
echo ""
echo "=== Checking alias directories ==="
for dir in "src/components" "src/lib" "src/components/ui" "src/hooks"; do
  if [ -d "$dir" ]; then
    echo "$dir exists"
  else
    echo "$dir not found"
  fi
done

# Check TypeScript config for path aliases
echo ""
echo "=== Checking tsconfig.json for path aliases ==="
if [ -f "tsconfig.json" ]; then
  echo "✓ tsconfig.json exists"
  jq '.compilerOptions.paths' tsconfig.json 2>/dev/null || grep -A 20 '"paths"' tsconfig.json
else
  echo "✗ tsconfig.json not found"
fi

Length of output: 926


Create the src/hooks directory or remove the hooks alias from components.json.

The ShadCN configuration defines a hooks alias ("hooks": "@/hooks") that maps to src/hooks, but this directory does not exist. This will cause import failures at runtime if the alias is referenced in the codebase. Either create the directory to match the configuration or remove the alias definition if hooks are not needed.

🤖 Prompt for AI Agents
In components.json around lines 1 to 22, the "hooks": "@/hooks" alias points to
a non-existent src/hooks directory which will break imports; either create the
src/hooks directory (add an index file or relevant hook files so the alias
resolves) or remove the "hooks" alias from components.json, and if you choose
removal, also update any import paths that reference "@/hooks" and ensure
tsconfig/paths (or other alias configs) remain consistent.

Comment on lines 177 to 201
<PiPlayPause
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (!actionRef.current) return;
if (actionRef.current?.paused) {
actionRef.current.paused = false;
} else {
actionRef.current.paused = true;
}
}}
>
Pause
</PiPlayPause>
<PiRepeat
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset();
actionRef.current.paused = false;
actionRef.current.play();
}
}}
>
Restart
</PiRepeat>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use real buttons for controls; fix inaccessible icon usage and invalid children.

Icons are not keyboard‑accessible and incorrectly include text as children. Replace with <button>s, add aria‑labels, and move the icons inside. This also improves focus styles.

Apply:

-        <PiPlayPause
-          className="text-primary w-10 h-10 cursor-pointer"
-          onClick={() => {
-            if (!actionRef.current) return;
-            if (actionRef.current?.paused) {
-              actionRef.current.paused = false;
-            } else {
-              actionRef.current.paused = true;
-            }
-          }}
-        >
-          Pause
-        </PiPlayPause>
-        <PiRepeat
-          className="text-primary w-10 h-10 cursor-pointer"
-          onClick={() => {
-            if (actionRef.current) {
-              actionRef.current.reset();
-              actionRef.current.paused = false;
-              actionRef.current.play();
-            }
-          }}
-        >
-          Restart
-        </PiRepeat>
+        <button
+          type="button"
+          aria-label="Play or pause animation"
+          className="text-primary w-10 h-10 inline-flex items-center justify-center"
+          onClick={() => {
+            const a = actionRef.current;
+            if (!a) return;
+            a.paused = !a.paused;
+          }}
+        >
+          <PiPlayPause aria-hidden="true" className="w-10 h-10" />
+        </button>
+        <button
+          type="button"
+          aria-label="Restart animation"
+          className="text-primary w-10 h-10 inline-flex items-center justify-center"
+          onClick={() => {
+            const a = actionRef.current;
+            if (!a) return;
+            a.reset();
+            a.paused = false;
+            a.play();
+          }}
+        >
+          <PiRepeat aria-hidden="true" className="w-10 h-10" />
+        </button>
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<PiPlayPause
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (!actionRef.current) return;
if (actionRef.current?.paused) {
actionRef.current.paused = false;
} else {
actionRef.current.paused = true;
}
}}
>
Pause
</PiPlayPause>
<PiRepeat
className="text-primary w-10 h-10 cursor-pointer"
onClick={() => {
if (actionRef.current) {
actionRef.current.reset();
actionRef.current.paused = false;
actionRef.current.play();
}
}}
>
Restart
</PiRepeat>
<button
type="button"
aria-label="Play or pause animation"
className="text-primary w-10 h-10 inline-flex items-center justify-center"
onClick={() => {
const a = actionRef.current;
if (!a) return;
a.paused = !a.paused;
}}
>
<PiPlayPause aria-hidden="true" className="w-10 h-10" />
</button>
<button
type="button"
aria-label="Restart animation"
className="text-primary w-10 h-10 inline-flex items-center justify-center"
onClick={() => {
const a = actionRef.current;
if (!a) return;
a.reset();
a.paused = false;
a.play();
}}
>
<PiRepeat aria-hidden="true" className="w-10 h-10" />
</button>
🤖 Prompt for AI Agents
In src/components/vrm.tsx around lines 177 to 201, the icon components are being
used as interactive controls with text children (invalid) and are not
keyboard‑accessible; replace each interactive icon usage with a semantic
<button> element (type="button") wrapping the icon component, move the icon to
be the button's child (remove text from the icon), add an appropriate aria-label
(e.g., "Pause" / "Restart"), keep the existing onClick logic on the button, and
ensure the button has the same styling classes plus a focus-visible style so
keyboard users can see focus.

Comment on lines 7 to 9
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Harden locale validation (avoid as any)

Do a proper type guard and fallback cleanly to the default locale.

-  if (!locale || !routing.locales.includes(locale as any)) {
-    locale = routing.defaultLocale;
-  }
+  const isSupportedLocale = (l: unknown): l is (typeof routing.locales)[number] =>
+    typeof l === "string" && (routing.locales as readonly string[]).includes(l);
+
+  if (!isSupportedLocale(locale)) {
+    locale = routing.defaultLocale;
+  }
🤖 Prompt for AI Agents
In src/i18n/request.ts around lines 7 to 9, the current check uses a loose cast
(as any) when validating locale; replace it with a proper type guard: verify
locale is a string (typeof locale === 'string') and that routing.locales
includes that string, otherwise assign routing.defaultLocale; ensure you do not
use any casts and keep types consistent with routing.locales' element type.

Comment on lines 11 to 21
if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add error handling, env validation, and cache semantics to fetches

Current code can throw on non-2xx and missing envs. Add guards, no-store in dev, and a revalidate in prod. Consider an AbortController for timeouts.

-  if (process.env.NODE_ENV === "development") {
-    const res = await fetch(
+  if (process.env.NODE_ENV === "development") {
+    const { TOLGEE_API_URL, TOLGEE_PROJECT_ID, TOLGEE_API_KEY } = process.env;
+    if (!TOLGEE_API_URL || !TOLGEE_PROJECT_ID || !TOLGEE_API_KEY) {
+      console.warn("Tolgee env vars missing; falling back to empty messages in dev");
+      return { locale, messages: {} };
+    }
+    const res = await fetch(
       `${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
       {
-        headers: {
+        cache: "no-store",
+        headers: {
           "x-api-key": `${process.env.TOLGEE_API_KEY}`,
         },
       },
     );
-    const data = await res.json();
+    if (!res.ok) {
+      console.warn("Tolgee fetch failed:", res.status, res.statusText);
+      return { locale, messages: {} };
+    }
+    const data = await res.json();
     const messages = data[locale];
 
     return {
       locale,
-      messages,
+      messages: messages ?? {},
     };
   }
 
-  const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);
+  const base = process.env.I18N_PUBLIC_URL;
+  if (!base) {
+    console.warn("I18N_PUBLIC_URL is missing; returning empty messages");
+    return { locale, messages: {} };
+  }
+  const res = await fetch(`${base}/${locale}.json`, { next: { revalidate: 3600 } });
+  if (!res.ok) {
+    console.warn("Static messages fetch failed:", res.status, res.statusText);
+    return { locale, messages: {} };
+  }
 
   return {
     locale,
     messages: await res.json(),
   };

Also applies to: 23-27, 29-34

🤖 Prompt for AI Agents
In src/i18n/request.ts around lines 11 to 21 (and also apply same changes to
lines 23-27 and 29-34), the fetch currently assumes required environment
variables exist, does not handle non-2xx responses, and has no cache semantics
or timeout; validate that NODE_ENV, TOLGEE_API_URL, TOLGEE_PROJECT_ID and
TOLGEE_API_KEY are present and throw or return a safe fallback when missing,
wrap the fetch in try/catch and check res.ok before calling res.json (throw or
log a descriptive error when not ok), add an AbortController with a short
timeout to cancel hung requests, and set fetch cache options to cache:
'no-store' when NODE_ENV === 'development' and to { next: { revalidate:
<seconds> } } (or equivalent) in production so dev fetches are fresh and prod
revalidates; ensure the same pattern (env validation, response.ok guard,
timeout, cache semantics, and safe fallback) is applied to the other indicated
line ranges.

Comment on lines +4 to +6
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Eliminate code duplication with src/components/cn.ts.

This cn function is identical to the one in src/components/cn.ts. Having duplicate utility functions violates the DRY principle and creates maintenance risks.

Recommended solution: Keep this implementation in src/lib/utils.ts and update src/components/cn.ts to re-export from here:

In src/components/cn.ts:

export { cn } from "@/lib/utils";

This centralises the utility in lib/utils.ts whilst maintaining backward compatibility for existing imports from components/cn.ts.

🤖 Prompt for AI Agents
In src/lib/utils.ts around lines 4 to 6, the cn function duplicates the
identical implementation in src/components/cn.ts; remove duplication by keeping
the implementation here and change src/components/cn.ts to re-export it. Update
src/components/cn.ts to export { cn } from "@/lib/utils"; so consumers importing
from components/cn.ts continue to work while the single implementation lives in
lib/utils.ts.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
package.json (2)

11-14: Remove orphaned deployment scripts.

These scripts reference opennextjs-cloudflare and wrangler which are no longer in the dependencies and will fail when executed.


53-53: Update React types to match React 19.

The @types/react version (^18) doesn't match the React version (19.2.0 on line 37), which can cause type errors with React 19 features.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7abd5fc and f4f6e38.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • src/assets/cloudfront.png is excluded by !**/*.png
📒 Files selected for processing (4)
  • imgLoader.ts (1 hunks)
  • next.config.mjs (1 hunks)
  • package.json (1 hunks)
  • src/imgLoader.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • src/imgLoader.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages

Comment on lines +1 to +2
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Validate required environment variables.

The loader relies on NEXT_PUBLIC_IMAGE_OPTIMIZER_URL and CF_PAGES_URL but doesn't validate their presence. If either is undefined or empty in production, the loader will generate malformed URLs, causing image loading failures.

Apply this diff to add validation:

 const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
 const hostname = process.env.CF_PAGES_URL;
 
+if (process.env.NODE_ENV !== "development") {
+  if (!optimizerUrl) {
+    throw new Error("NEXT_PUBLIC_IMAGE_OPTIMIZER_URL is required in production");
+  }
+  if (!hostname) {
+    throw new Error("CF_PAGES_URL is required in production");
+  }
+}
+
 export default function ImageLoader({
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || "";
const hostname = process.env.CF_PAGES_URL;
if (process.env.NODE_ENV !== "development") {
if (!optimizerUrl) {
throw new Error("NEXT_PUBLIC_IMAGE_OPTIMIZER_URL is required in production");
}
if (!hostname) {
throw new Error("CF_PAGES_URL is required in production");
}
}
export default function ImageLoader({
🤖 Prompt for AI Agents
In imgLoader.ts around lines 1 to 2, the environment variables
NEXT_PUBLIC_IMAGE_OPTIMIZER_URL and CF_PAGES_URL are read but not validated; add
explicit checks after reading them to ensure they are non-empty strings and
throw a clear, early error (or call a startup logger and exit) if either is
missing so the loader never constructs malformed URLs; include which variable is
missing in the error message and fail fast in production builds to surface
configuration problems immediately.

}
const params = [`size=${width}`];
params.push(`quality=${quality || 75}`);
return `${optimizerUrl}/${encodeURIComponent(`${hostname}${src}`)}?${params.join("&")}`;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fix URL construction to handle edge cases.

The URL construction has several potential issues:

  1. CF_PAGES_URL may not include a protocol (e.g., "example.com" instead of "https://example.com")
  2. Concatenating ${hostname}${src} assumes hostname doesn't end with / and src starts with /
  3. If either assumption is violated, you'll get malformed URLs like "example.com//path" or "example.compath"

Apply this diff to make URL construction more robust:

+  const normalizedHostname = hostname?.replace(/\/$/, "") || "";
+  const normalizedSrc = src.startsWith("/") ? src : `/${src}`;
+  const fullPath = `${normalizedHostname}${normalizedSrc}`;
+
   const params = [`size=${width}`];
   params.push(`quality=${quality || 75}`);
-  return `${optimizerUrl}/${encodeURIComponent(`${hostname}${src}`)}?${params.join("&")}`;
+  return `${optimizerUrl}/${encodeURIComponent(fullPath)}?${params.join("&")}`;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In imgLoader.ts around line 18, the URL is built by simply concatenating
optimizerUrl, hostname and src which breaks when CF_PAGES_URL lacks a protocol,
or when hostname ends with '/' or src starts with '/' (producing double slashes
or missing slash). Fix by normalizing the host to include a protocol (default to
"https://" if none), normalize/truncate leading/trailing slashes on hostname and
src (or use the URL API to set pathname) so they join with a single slash, and
ensure you pass a properly encoded path to encodeURIComponent or encode the full
URL via new URL(...) before encoding; then rebuild the final optimizer URL using
the normalized base so params.join("&") remains appended correctly.

next.config.mjs Outdated
output: "export",
images: {
loader: "custom",
loaderFile: "./imgLoader.ts",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Incorrect path to image loader file.

The loaderFile path is set to "./imgLoader.ts", but based on the file structure, the loader is located at "./src/imgLoader.ts". This mismatch will cause a build failure when Next.js attempts to load the custom image loader.

Apply this diff to correct the path:

-    loaderFile: "./imgLoader.ts",
+    loaderFile: "./src/imgLoader.ts",
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
loaderFile: "./imgLoader.ts",
loaderFile: "./src/imgLoader.ts",
🤖 Prompt for AI Agents
In next.config.mjs around line 11, the loaderFile path is incorrect
("./imgLoader.ts") and should point to "./src/imgLoader.ts"; update the
loaderFile value to "./src/imgLoader.ts" so Next.js can locate the custom image
loader and avoid build failure.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (4)
package.json (1)

50-51: Update React type definitions to match React 19.

The project uses React 19.2.0 but @types/react is pinned to ^18, which is incompatible. React 19 requires @types/react@^19.0.0 and @types/react-dom@^19.0.0.

Apply this diff:

-    "@types/react": "^18",
-    "@types/react-dom": "^18",
+    "@types/react": "^19",
+    "@types/react-dom": "^19",

This mismatch will cause TypeScript errors with React 19 APIs and prevent proper type checking.

src/app/[locale]/page.tsx (3)

9-9: Remove unused translation to eliminate lint noise.

The translation function t is loaded but never used in the component, creating unnecessary overhead and potential linting warnings.

Either remove the unused import:

-  const t = await getTranslations("home");
   return (

Or use it to translate the button text:

   const t = await getTranslations("home");
   return (
     <>
-      <Button>a</Button>
+      <Button>{t("hi")}</Button>
     </>
   );

4-7: Incorrect params signature for Next.js App Router pages.

Next.js App Router pages receive an object containing params, not a Promise directly. The current signature will cause locale to be undefined at runtime, breaking locale initialisation.

Apply this diff to fix the signature:

-export default async function IndexPage(params: Promise<{ locale: string }>) {
-  const { locale } = await params;
+export default async function IndexPage({ params }: { params: Promise<{ locale: string }> }) {
+  const { locale } = await params;

Based on the past review comment, though note that in Next.js 15+, params is typically a Promise that should be awaited.


12-12: Button text appears to be a placeholder.

The button displays "a", which seems like placeholder content rather than final implementation.

Consider using a more meaningful label or the loaded translation:

-      <Button>a</Button>
+      <Button>{t("hi")}</Button>
🧹 Nitpick comments (2)
src/app/[locale]/template.tsx (2)

8-12: Simplify redundant wrapper structure.

The fragment wrapping a plain div adds unnecessary nesting. Since neither wrapper serves a functional purpose (no props, styling, or event handlers), consider returning children directly.

Apply this diff to simplify:

-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return children;

Alternatively, if you need a single wrapper for future styling or layout purposes, keep just the div:

-  return (
-    <>
-      <div>{children}</div>
-    </>
-  );
+  return <div>{children}</div>;

6-6: Consider using environment variables for configuration.

The Swetrix ID and API URL are hardcoded, which reduces flexibility across different environments (development, staging, production). Consider moving these to environment variables.

Example refactor:

-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+  useSwetrix(process.env.NEXT_PUBLIC_SWETRIX_ID!, { 
+    apiURL: process.env.NEXT_PUBLIC_SWETRIX_API_URL! 
+  });

Then add to your .env.local:

NEXT_PUBLIC_SWETRIX_ID=XxNIMaHCaVG3
NEXT_PUBLIC_SWETRIX_API_URL=https://analytics.mikandev.tech/log
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f4f6e38 and 01c4b09.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • package.json (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/components/fancy/NumberTicker.tsx (0 hunks)
  • src/components/fancy/code-comp.tsx (0 hunks)
  • src/components/fancy/icon-cloud.tsx (0 hunks)
  • src/components/fancy/marqee.tsx (0 hunks)
  • src/components/fancy/typewriter.tsx (0 hunks)
  • src/components/nUI/Footer.tsx (0 hunks)
  • src/components/nUI/Header.tsx (0 hunks)
  • src/components/nUI/MenuToggle.tsx (0 hunks)
  • src/components/nUI/hooks.tsx (0 hunks)
  • src/i18n/request.ts (1 hunks)
  • src/messages/en.json (1 hunks)
  • tsconfig.json (1 hunks)
💤 Files with no reviewable changes (9)
  • src/components/nUI/MenuToggle.tsx
  • src/components/fancy/icon-cloud.tsx
  • src/components/nUI/Footer.tsx
  • src/components/fancy/marqee.tsx
  • src/components/fancy/NumberTicker.tsx
  • src/components/fancy/typewriter.tsx
  • src/components/nUI/hooks.tsx
  • src/components/nUI/Header.tsx
  • src/components/fancy/code-comp.tsx
🧰 Additional context used
🧬 Code graph analysis (1)
src/app/[locale]/page.tsx (1)
src/components/ui/button.tsx (1)
  • Button (57-57)
🔇 Additional comments (1)
src/messages/en.json (1)

2-4: Intentional simplification to minimal translation structure.

The translation keys have been drastically reduced to a minimal placeholder. This aligns with the refactor's goal to simplify i18n handling and move to local file-based translations.

Comment on lines +3 to +9
export default getRequestConfig(async () => {
const locale = "en";

if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}

if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];

return {
locale,
messages,
};
}

const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);

return {
locale,
messages: await res.json(),
};
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Hardcoded locale ignores route parameter and breaks dynamic locale support.

The locale is hardcoded to "en", which means the [locale] route parameter in src/app/[locale]/page.tsx is effectively ignored for message loading. Whilst the page calls setRequestLocale(locale) with the URL parameter, the messages will always be loaded for English regardless of which locale is requested in the URL.

If the intention is to make this a monolingual application, consider either:

  1. Removing the [locale] dynamic route segment entirely, or
  2. Using the locale from request context to support the existing multi-locale routing structure.

If multi-locale support is intended, apply this diff:

-export default getRequestConfig(async () => {
-  const locale = "en";
+export default getRequestConfig(async ({ requestLocale }) => {
+  let locale = await requestLocale;
+  
+  // Fallback to "en" if no locale provided
+  if (!locale) {
+    locale = "en";
+  }

   return {
     locale,
     messages: (await import(`../messages/${locale}.json`)).default,
   };
 });

Note: The past review comments regarding error handling and locale validation are now obsolete as they referenced the removed remote fetch logic.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default getRequestConfig(async () => {
const locale = "en";
if (!locale || !routing.locales.includes(locale as any)) {
locale = routing.defaultLocale;
}
if (process.env.NODE_ENV === "development") {
const res = await fetch(
`${process.env.TOLGEE_API_URL}/v2/projects/${process.env.TOLGEE_PROJECT_ID}/translations/${locale}`,
{
headers: {
"x-api-key": `${process.env.TOLGEE_API_KEY}`,
},
},
);
const data = await res.json();
const messages = data[locale];
return {
locale,
messages,
};
}
const res = await fetch(`${process.env.I18N_PUBLIC_URL}/${locale}.json`);
return {
locale,
messages: await res.json(),
};
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
export default getRequestConfig(async ({ requestLocale }) => {
let locale = await requestLocale;
// Fallback to "en" if no locale provided
if (!locale) {
locale = "en";
}
return {
locale,
messages: (await import(`../messages/${locale}.json`)).default,
};
});

],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Disabling strict mode represents a type-safety regression.

Setting "strict": false disables all strict TypeScript checks, which undermines the benefits of upgrading to React 19's improved type definitions. This is especially problematic during a major version upgrade where stricter typing helps catch incompatibilities early.

Recommend reverting to "strict": true and addressing any type errors that emerge, rather than suppressing type checking altogether.

🤖 Prompt for AI Agents
In tsconfig.json around line 11, "strict" is set to false which disables all
TypeScript strict checks; change "strict" to true and rebuild/type-check, then
fix resulting type errors (address mismatched React 19 types, add explicit
types, narrow any/unknown usages, and update code or third‑party types as
needed) until the project compiles cleanly; if specific strict subflags are
problematic, enable strict and selectively relax only the minimal needed flags
(e.g., noImplicitAny or strictNullChecks) after resolving as many issues as
possible.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 22

♻️ Duplicate comments (3)
tsconfig.json (1)

7-7: ⚠️ Type-safety regression: strict mode remains disabled.

This mirrors the concern raised in the previous review. Setting "strict": false disables all TypeScript strict checks, which is especially problematic during a major React 19 migration. Rather than suppressing type checking, enable strict mode and resolve any type errors that emerge. If specific strict subflags prove problematic, selectively relax only the minimal required flags (e.g., noImplicitAny or strictNullChecks) after addressing as many issues as possible.

src/app/[locale]/page.tsx (2)

4-7: Incorrect page props signature; App Router passes { params }, not a Promise.

Accept { params } and read locale from it.

-export default async function IndexPage(params: Promise<{ locale: string }>) {
-  const { locale } = await params;
+export default async function IndexPage({ params }: { params: { locale: string } }) {
+  const { locale } = params;

9-14: Use or remove the loaded translation.

Either render a translated label or drop getTranslations to avoid lint noise.

Option A (use it):

-  const t = await getTranslations("home");
+  const t = await getTranslations("home");
   return (
     <>
-      <Button>Hello there!</Button>
+      <Button>{t("cta")}</Button>
     </>
   );

Option B (remove it):

-  const t = await getTranslations("home");
   return (
     <>
       <Button>Hello there!</Button>
     </>
   );
🧹 Nitpick comments (33)
src/components/FluidGlass.tsx (5)

30-37: Strengthen ModeProps typing and drop broad Record<string, unknown>

Current typing weakens safety (e.g. navItems is cast later). Define a concrete shape for material knobs + navItems to regain type help.

-type ModeProps = Record<string, unknown>;
+type ModeProps = {
+  // optional navigation for "bar" mode
+  navItems?: NavItem[];
+  // MeshTransmissionMaterial controls
+  scale?: number;
+  ior?: number;
+  thickness?: number;
+  anisotropy?: number;
+  chromaticAberration?: number;
+  transmission?: number;
+  roughness?: number;
+  attenuationColor?: string | number | THREE.Color;
+  attenuationDistance?: number;
+  color?: string | number | THREE.Color;
+};
-  } = modeProps as {
-    scale?: number;
-    ior?: number;
-    thickness?: number;
-    anisotropy?: number;
-    chromaticAberration?: number;
-    [key: string]: unknown;
-  };
+  } = modeProps as ModeProps;

Also applies to: 140-155


159-162: Background plane material flags

This plane is just the offscreen buffer. Disable tone mapping and depth writes to avoid subtle colour/ordering artefacts.

-        <meshBasicMaterial map={buffer.texture} transparent />
+        <meshBasicMaterial
+          map={buffer.texture}
+          transparent
+          toneMapped={false}
+          depthWrite={false}
+        />

185-206: Preload GLBs to avoid first‑interaction hitch

Proactively warm the cache for known assets.

+// Preload GLTF assets
+useGLTF.preload("https://cdn.mikn.dev/web/static/lens.glb");
+useGLTF.preload("https://cdn.mikn.dev/web/static/cube.glb");
+useGLTF.preload("https://cdn.mikn.dev/web/static/bar.glb");

Place these at module scope (e.g. end of file).

Also applies to: 209-230


270-275: Navigation approach in SPA frameworks

Directly mutating window.location bypasses client routing. Consider your router (next/router, react-router) for smoother transitions and prefetch.


63-69: <Scroll html /> with no children

This mounts an empty HTML overlay. Remove if unused.

src/interfaces/CursorToys.ts (1)

3-6: Name the props and export the union for reuse

Prefer CursorToysProps and a reusable union type to avoid string literals scattered across the codebase.

 export interface ICursorToys {
   children?: ReactNode;
-  selectedToy?: "none" | "spark" | "splash" | "custom" | "target";
+  selectedToy?: CursorToyName;
 }
+
+export type CursorToyName = "none" | "spark" | "splash" | "custom" | "target";
src/lib/get-strict-context.tsx (1)

15-24: Set displayName for easier debugging and clearer error messages

Small DX upgrade: display names improve React DevTools and errors.

   const Context = React.createContext<T | undefined>(undefined);
+  Context.displayName = `${name ?? "Anonymous"}Context`;
 
   const Provider = ({
     value,
     children,
   }: {
     value: T;
     children?: React.ReactNode;
   }) => <Context.Provider value={value}>{children}</Context.Provider>;
+  Provider.displayName = `${name ?? "Anonymous"}Provider`;
 
   const useSafeContext = () => {
     const ctx = React.useContext(Context);
     if (ctx === undefined) {
-      throw new Error(`useContext must be used within ${name ?? "a Provider"}`);
+      throw new Error(
+        `${name ?? "useContext"} must be used within ${name ?? "Provider"}`,
+      );
     }
     return ctx;
   };

Also applies to: 25-31

src/hooks/use-is-in-view.tsx (1)

10-17: Type the ref as T | null and drop the unsafe cast

The current cast can mask nulls. Align the ref types and forward the instance directly.

-function useIsInView<T extends HTMLElement = HTMLElement>(
-  ref: React.Ref<T>,
+function useIsInView<T extends HTMLElement = HTMLElement>(
+  ref: React.Ref<T | null>,
   options: UseIsInViewOptions = {},
 ) {
   const { inView, inViewOnce = false, inViewMargin = "0px" } = options;
   const localRef = React.useRef<T>(null);
-  React.useImperativeHandle(ref, () => localRef.current as T);
+  React.useImperativeHandle(ref, () => localRef.current);
src/components/animate-ui/primitives/radix/radio-group.tsx (1)

15-18: Remove redundant state; derive isChecked from context

isChecked can be computed from the group value. This avoids duplicated state and the extra setter in context.

-type RadioGroupItemContextType = {
-  isChecked: boolean;
-  setIsChecked: (isChecked: boolean) => void;
-};
+type RadioGroupItemContextType = { isChecked: boolean };
@@
 function RadioGroupItem({
   value: valueProps,
   disabled,
   required,
   ...props
 }: RadioGroupItemProps) {
   const { value } = useRadioGroup();
-  const [isChecked, setIsChecked] = React.useState(value === valueProps);
-
-  React.useEffect(() => {
-    setIsChecked(value === valueProps);
-  }, [value, valueProps]);
+  const isChecked = value === valueProps;
@@
   return (
-    <RadioGroupItemProvider value={{ isChecked, setIsChecked }}>
+    <RadioGroupItemProvider value={{ isChecked }}>

This also lets you simplify RadioGroupIndicator’s context type (no setter).

Also applies to: 87-98, 100-116, 119-130

src/components/animate-ui/icons/icon.tsx (1)

620-645: Avoid calling hooks in non-hook helpers

getVariants calls a hook from a plain function (eslint disabled). Prefer a hook wrapper or pass animationType in.

-function getVariants<
+function useVariants<
   V extends { default: T; [key: string]: T },
   T extends Record<string, Variants>,
 >(animations: V): T {
-  // eslint-disable-next-line react-hooks/rules-of-hooks
-  const { animation: animationType } = useAnimateIconContext();
+  const { animation: animationType } = useAnimateIconContext();

And update call sites accordingly (or refactor to getVariants(animations, animationType)).
This keeps the hooks rules intact.

src/components/GlobalCursorToys.tsx (1)

2-9: Code‑split TargetCursor to trim the default bundle.

Static import ships TargetCursor even when unused. Use a dynamic import with SSR disabled for this purely client visual.

Apply:

 "use client";
-import { useCursorToys } from "@/contexts/CursorToysContext";
-import TargetCursor from "@/components/TargetCursor";
+import { useCursorToys } from "@/contexts/CursorToysContext";
+import dynamic from "next/dynamic";
+const TargetCursor = dynamic(
+  () => import("@/components/TargetCursor"),
+  { ssr: false }
+);
src/components/CustomCursor.tsx (1)

9-22: Reduce re-render pressure from mousemove.

State updates on every mousemove can thrash the React tree. Throttle via requestAnimationFrame or update a ref and animate with CSS transforms.

If you want, I can provide a small rAF-based snippet to drop re-renders to ~60/sec instead of device rate.

src/components/consent-manager.tsx (1)

8-20: LGTM; consider making options overridable.

Solid wrapper. To future‑proof, allow options as a prop with a default of { mode: "offline" }.

src/app/layout.tsx (1)

5-13: You declared a font CSS variable but didn’t apply it.

You set variable: "--font-sans" but only use hsr.className. Either remove variable or apply it.

-    <html className={hsr.className}>
+    <html lang="en" className={`${hsr.className} ${hsr.variable}`}>
.idea/copilotDiffState.xml (1)

1-17: Remove IDE state files from version control.

Verification confirms that .idea/ is neither ignored nor excluded from tracking. Currently, 12 .idea/ files are tracked in Git, including .idea/copilotDiffState.xml. These environment-specific files should not be committed.

  • Add to .gitignore:
+/.idea/
  • Remove from Git history: git rm -r --cached .idea
src/components/animate-ui/primitives/buttons/button.tsx (2)

18-33: Forward ref for focus and integrations

Expose a ref so consumers can imperatively focus the button and integrate with tooltips/menus.

-function Button({
+const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(
+  {
   hoverScale = 1.05,
   tapScale = 0.95,
   asChild = false,
   ...props
-}: ButtonProps) {
+}: ButtonProps, ref) {
   const Component = asChild ? Slot : motion.button;
 
   return (
     <Component
+      ref={ref}
       whileTap={{ scale: tapScale }}
       whileHover={{ scale: hoverScale }}
       {...props}
     />
   );
 }
 
 export { Button, type ButtonProps };

11-16: Polymorphic typing for asChild

ButtonProps are bound to HTMLMotionProps<"button"> even when rendering non-button children. Consider a polymorphic prop pattern (e.g., generic T extends ElementType) so the props match the rendered element when asChild is used.

src/components/SettingsController.tsx (2)

44-49: Add dialog semantics to the panel

Improve accessibility by marking the floating panel as a dialog and linking it to the trigger. Optionally manage focus on open/close.

-          <motion.div
+          <motion.div
+            role="dialog"
+            aria-modal="true"
+            id="site-settings-panel"
             initial={{ opacity: 0, y: 7 }}
             animate={{ opacity: 1, y: 0 }}
             exit={{ opacity: 0, y: 7 }}
             className="absolute bottom-full mb-5 bg-white shadow-lg rounded-lg p-4 w-80"
           >

9-11: Drop children from this component

children are rendered inside a fixed container, which is surprising and likely unused. Simplify the API by removing children from props and JSX.

-interface AccButtonProps {
-  children?: React.ReactNode;
-}
+interface AccButtonProps {}
...
-      {children}

Also applies to: 96-97

src/contexts/CursorToysContext.tsx (1)

27-30: Harden localStorage write

localStorage.setItem can throw (quota, privacy mode). Add a try/catch to avoid crashing UI.

   const setSelectedToy = (value: SelectedToy) => {
     setSelectedToyState(value);
-    localStorage.setItem("cursorToy", value);
+    try {
+      localStorage.setItem("cursorToy", value);
+    } catch {
+      // no-op
+    }
   };
src/app/[locale]/layout.tsx (1)

19-22: Fix params typing (no Promise) and drop unnecessary await

Next.js passes params as a plain object. Correct the type and remove await for clarity.

 interface LocaleLayoutProps {
-  children: ReactNode;
-  params: Promise<{ locale: string }>;
+  children: ReactNode;
+  params: { locale: string };
 }
 
 export default async function LocaleLayout({
   children,
   params,
 }: LocaleLayoutProps) {
-  const { locale } = await params;
+  const { locale } = params;

Also applies to: 28-36

src/components/animate-ui/icons/settings.tsx (1)

55-66: Consider default icon accessibility props (if IconWrapper doesn’t inject them).

Recommend adding aria-hidden="true" focusable="false" by default, and supporting an optional title/aria-label for labelled usage.

-    <motion.svg
+    <motion.svg
       xmlns="http://www.w3.org/2000/svg"
       width={size}
       height={size}
       viewBox="0 0 24 24"
       fill="none"
       stroke="currentColor"
       strokeWidth={2}
       strokeLinecap="round"
       strokeLinejoin="round"
+      aria-hidden={props["aria-label"] ? undefined : true}
+      focusable="false"
       {...props}
     >
src/components/animate-ui/components/buttons/button.tsx (1)

48-54: Prevent accidental form submits with a default type.

Unless ButtonPrimitive already sets it, add type="button" by default.

-    <ButtonPrimitive
+    <ButtonPrimitive
+      type={(props as any).type ?? "button"}
       className={cn(buttonVariants({ variant, size }), className)}
       {...props}
     />

Please confirm whether the primitive already applies a default.

src/components/ClickSpark.tsx (2)

45-51: DPR-aware canvas for crisp lines on HiDPI.

Scale the backing store by devicePixelRatio and scale the context before drawing.

-      const { width, height } = parent.getBoundingClientRect();
-      if (canvas.width !== width || canvas.height !== height) {
-        canvas.width = width;
-        canvas.height = height;
-      }
+      const { width, height } = parent.getBoundingClientRect();
+      const dpr = window.devicePixelRatio || 1;
+      const pixelW = Math.floor(width * dpr);
+      const pixelH = Math.floor(height * dpr);
+      if (canvas.width !== pixelW || canvas.height !== pixelH) {
+        canvas.width = pixelW;
+        canvas.height = pixelH;
+        canvas.style.width = `${width}px`;
+        canvas.style.height = `${height}px`;
+      }

And in the draw loop (once per frame, before drawing):

-      ctx.clearRect(0, 0, canvas.width, canvas.height);
+      const dpr = window.devicePixelRatio || 1;
+      ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
+      ctx.clearRect(0, 0, canvas.width, canvas.height);

Also applies to: 85-96


34-35: Remove unused startTimeRef.

It’s set but never read; drop it to reduce noise.

-  const startTimeRef = useRef<number | null>(null);
@@
-      if (!startTimeRef.current) {
-        startTimeRef.current = timestamp;
-      }
@@
-    return () => {
-      cancelAnimationFrame(animationId);
-    };
+    return () => {
+      cancelAnimationFrame(animationId);
+    };

Also applies to: 93-97, 131-133

src/components/animate-ui/primitives/animate/slot.tsx (1)

38-59: Compose event handlers instead of overriding.

mergeProps overwrites handlers like onClick/onKeyDown. Consider composing them so both fire.

 function mergeProps<T extends HTMLElement>(
   childProps: AnyProps,
   slotProps: DOMMotionProps<T>,
 ): AnyProps {
   const merged: AnyProps = { ...childProps, ...slotProps };
@@
+  // compose common event handlers if both provided
+  const handlers = [
+    "onClick","onMouseEnter","onMouseLeave","onFocus","onBlur","onKeyDown","onKeyUp",
+  ] as const;
+  handlers.forEach((key) => {
+    const c = childProps[key as keyof AnyProps] as ((e: any) => void) | undefined;
+    const s = slotProps[key as keyof DOMMotionProps<T>] as ((e: any) => void) | undefined;
+    if (c && s) {
+      merged[key] = (e: any) => {
+        c(e);
+        if (!e?.defaultPrevented) s(e);
+      };
+    }
+  });
src/components/TargetCursor.tsx (2)

274-326: Clear pending resume timeout on unmount to avoid late execution.

Ensure any scheduled resume is cancelled during cleanup.

     return () => {
       window.removeEventListener("mousemove", moveHandler);
       window.removeEventListener("mouseover", enterHandler);
       window.removeEventListener("scroll", scrollHandler);
 
       if (activeTarget) {
         cleanupTarget(activeTarget);
       }
 
+      if (resumeTimeout) {
+        clearTimeout(resumeTimeout);
+        resumeTimeout = null;
+      }
+
       spinTl.current?.kill();
       document.body.style.cursor = originalCursor;
     };

41-45: Cursor restore robustness across prop flips.

If hideDefaultCursor toggles during the component’s life, you may restore “none”. Consider storing the pre-hide value in a ref outside the effect.

-    const originalCursor = document.body.style.cursor;
+    const originalRef = { current: document.body.style.cursor };
     if (hideDefaultCursor) {
       document.body.style.cursor = "none";
     }
@@
-      document.body.style.cursor = originalCursor;
+      document.body.style.cursor = originalRef.current;
src/components/SplashCursor.tsx (5)

106-113: Respect the TRANSPARENT prop for context alpha.

Currently alpha is always true. Use the provided prop.

-      const params = {
-        alpha: true,
+      const params = {
+        alpha: TRANSPARENT,
         depth: false,
         stencil: false,
         antialias: false,
         preserveDrawingBuffer: false,
       };

56-70: Over‑high DYE_RESOLUTION (1440) will allocate very large dye FBOs (e.g., ~2.5k×1.4k on 16:9).

Lower the default and clamp by device size to avoid OOM and jank on mobiles. Manual filtering fallback alone isn’t sufficient.

@@
-  DYE_RESOLUTION = 1440,
+  DYE_RESOLUTION = 512,
@@
-    function getResolution(resolution: number) {
+    function getResolution(resolution: number) {
       const w = gl.drawingBufferWidth;
       const h = gl.drawingBufferHeight;
       const aspectRatio = w / h;
       let aspect = aspectRatio < 1 ? 1 / aspectRatio : aspectRatio;
-      const min = Math.round(resolution);
-      const max = Math.round(resolution * aspect);
+      const maxSideCap = Math.min(1024, Math.max(128, Math.round(resolution)));
+      const min = maxSideCap;
+      const max = Math.round(maxSideCap * aspect);
       if (w > h) {
         return { width: max, height: min };
       }
       return { width: min, height: max };
     }

If you prefer, gate higher resolutions behind a media check: prefers-reduced-motion or device memory.

Also applies to: 969-980, 884-914


1282-1286: SPLAT_RADIUS appears to be scaled twice (÷100); default 0.2 becomes 0.002. Intentional?

If SPLAT_RADIUS is already a [0..1] ratio, drop the /100.

-          splatProgram.uniforms.radius,
-          correctRadius(config.SPLAT_RADIUS / 100)!,
+          splatProgram.uniforms.radius,
+          correctRadius(config.SPLAT_RADIUS)!,

Alternatively, set default SPLAT_RADIUS to 20 (percent) if you want percentage semantics.

Also applies to: 64-65


195-216: GPU resource lifetime not managed; textures/FBOs aren’t deleted on resize or unmount.

Not deleting old textures/framebuffers can accumulate GPU memory. Add delete calls in resize and effect cleanup.

Happy to draft a small disposer (deleteTexture/deleteFramebuffer) and integrate it into resizeFBO/resizeDoubleFBO and the effect cleanup.

Also applies to: 218-257, 759-838, 840-882


1496-1510: Minor: unused/under‑used props.

  • BACK_COLOR and CAPTURE_RESOLUTION are defined but unused.
  • TRANSPARENT only applied if you accept the earlier alpha change.
    Consider removing or wiring them through (e.g., clear to BACK_COLOR on the default framebuffer).
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 01c4b09 and e04bede.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (29)
  • .idea/copilotDiffState.xml (1 hunks)
  • components.json (1 hunks)
  • package.json (1 hunks)
  • src/app/[locale]/layout.tsx (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/globals.css (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/components/ClickSpark.tsx (1 hunks)
  • src/components/CursorToys.tsx (1 hunks)
  • src/components/CustomCursor.tsx (1 hunks)
  • src/components/FluidGlass.tsx (1 hunks)
  • src/components/GlobalCursorToys.tsx (1 hunks)
  • src/components/SettingsController.tsx (1 hunks)
  • src/components/SplashCursor.tsx (1 hunks)
  • src/components/TargetCursor.tsx (1 hunks)
  • src/components/animate-ui/components/buttons/button.tsx (1 hunks)
  • src/components/animate-ui/icons/icon.tsx (1 hunks)
  • src/components/animate-ui/icons/settings.tsx (1 hunks)
  • src/components/animate-ui/primitives/animate/slot.tsx (1 hunks)
  • src/components/animate-ui/primitives/buttons/button.tsx (1 hunks)
  • src/components/animate-ui/primitives/radix/radio-group.tsx (1 hunks)
  • src/components/consent-manager.tsx (1 hunks)
  • src/contexts/CursorToysContext.tsx (1 hunks)
  • src/hooks/use-controlled-state.tsx (1 hunks)
  • src/hooks/use-is-in-view.tsx (1 hunks)
  • src/interfaces/CursorToys.ts (1 hunks)
  • src/lib/get-strict-context.tsx (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • components.json
  • src/app/globals.css
  • package.json
🧰 Additional context used
🧬 Code graph analysis (9)
src/components/GlobalCursorToys.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/CursorToys.tsx (4)
src/interfaces/CursorToys.ts (1)
  • ICursorToys (3-6)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/SplashCursor.tsx (1)
  • SplashCursor (55-1521)
src/components/CustomCursor.tsx (1)
  • CustomCursor (6-35)
src/components/animate-ui/components/buttons/button.tsx (1)
src/components/animate-ui/primitives/buttons/button.tsx (2)
  • ButtonProps (35-35)
  • Button (35-35)
src/components/SettingsController.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
src/components/animate-ui/primitives/radix/radio-group.tsx (2)
src/lib/get-strict-context.tsx (1)
  • getStrictContext (36-36)
src/hooks/use-controlled-state.tsx (1)
  • useControlledState (9-33)
src/components/animate-ui/primitives/buttons/button.tsx (2)
src/components/animate-ui/components/buttons/button.tsx (2)
  • ButtonProps (57-57)
  • Button (57-57)
src/components/animate-ui/primitives/animate/slot.tsx (2)
  • WithAsChild (94-94)
  • Slot (92-92)
src/app/[locale]/template.tsx (2)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/SettingsController.tsx (1)
  • SettingsController (34-99)
src/app/layout.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • CursorToysProvider (15-37)
src/app/[locale]/layout.tsx (2)
src/i18n/routing.ts (1)
  • routing (4-10)
src/components/consent-manager.tsx (1)
  • ConsentManager (8-20)
🔇 Additional comments (12)
tsconfig.json (2)

15-15: ✓ JSX runtime update is appropriate.

Changing from "preserve" to "react-jsx" aligns with React 19 and modern Next.js setups, eliminating the need for JSX factory imports. This complements the broader framework upgrade.


25-33: Verify that narrowed exclude does not inadvertently include build artefacts.

The updated include paths appropriately prioritise Next.js-generated types. However, the exclude array has been narrowed to only ["node_modules"], removing previous broader exclusions (including legacy sst.config.ts). This risks unintentionally including build output directories (e.g., .next compiled output, dist, build, coverage, out) if they exist. Whilst .next is typically generated during dev, other directories may linger.

Verify that all necessary directories are either excluded or not present in the working tree after build steps.

src/components/FluidGlass.tsx (2)

286-289: No changes required—outlineBlur="20%" is supported

Troika's outlineBlur prop accepts percentage strings like "20%", where the percentage is treated as a percentage of the fontSize. The existing code at lines 286-289 and 374-377 is correct and requires no modification.

Likely an incorrect or invalid review comment.


170-179: Code is correct per @react-three/drei API

The code properly matches the documented API. buffer is correctly instantiated via useFBO() (line 68), and buffer.texture (a THREE.Texture) is appropriately passed to MeshTransmissionMaterial's buffer prop on line 170. No changes required.

src/components/animate-ui/primitives/radix/radio-group.tsx (1)

4-4: Confirm "radix-ui" import path works and is intentional

The import path differs from the codebase's established pattern. Other Radix components use "@radix-ui/react-*" packages (e.g., "@radix-ui/react-slot"), but this file imports from "radix-ui". Whilst "radix-ui" v1.4.3 is listed in package.json and is a legitimate npm package, the official Radix package is "@radix-ui/react-radio-group". Please verify that the RadioGroup export from "radix-ui" works correctly at runtime and confirm whether this non-standard import is intentional, or switch to the official "@radix-ui/react-radio-group" package to maintain consistency across the codebase.

src/components/CustomCursor.tsx (1)

10-21: Cursor restore race if multiple custom cursors mount.

Setting document.body.style.cursor = "auto" on unmount can override another active toy. If multiple toys are possible, guard with a ref/count or centralise cursor hiding in the provider.

Is there any path where two cursor toys can mount simultaneously? If yes, I’ll propose a tiny centralised toggle in CursorToysContext.

src/app/layout.tsx (1)

3-15: No issues detected—provider correctly configured as client component.

Verification confirms that src/contexts/CursorToysContext.tsx has "use client" as the first line, allowing it to safely use React state and hooks as required.

src/components/GlobalCursorToys.tsx (1)

5-13: No issues found. Component is not used anywhere in the codebase.

Verification confirms that GlobalCursorToys is never imported or rendered—it exists only as a definition in src/components/GlobalCursorToys.tsx. The CursorToysProvider is correctly wrapping the application in src/app/layout.tsx, and the useCursorToys() hook includes proper error handling that throws when used outside the provider. The concern about independent route mounts is not applicable since the component is currently unused.

src/components/animate-ui/primitives/buttons/button.tsx (1)

24-31: asChild + Framer Motion compatibility

When asChild is true, whileHover/whileTap will only work if the child is a motion component and accepts Motion props; otherwise these props may leak to the DOM (React warnings) or be ignored. Consider wrapping Slot with motion (if compatible) or document that the child must be a motion.* element.

src/components/CursorToys.tsx (1)

11-26: LGTM — simple, readable selection logic

The conditional rendering is clear and keeps children always present. No issues spotted.

src/app/[locale]/template.tsx (1)

10-15: Provider correctly wraps the template

The CursorToysProvider is properly mounted in the root layout (src/app/layout.tsx) and wraps all child routes, including the [locale] template. In Next.js, the root layout's provider context cascades through all nested layouts and templates, so both CursorToys and SettingsController can safely call useCursorToys(). The code is correct as-is.

src/components/animate-ui/icons/settings.tsx (1)

15-48: Animations object looks solid and type-safe.

Good use of satisfies to constrain per‑part variants and keyof typeof animations for props. No issues spotted.

Comment on lines 12 to 16
<html className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add lang to for a11y and SEO.

Root <html> lacks a language attribute.

Apply:

-    <html className={hsr.className}>
+    <html lang="en" className={hsr.className}>

If you have per‑locale routing, pass the locale here instead of hard‑coding.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<html className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
<html lang="en" className={hsr.className}>
<body>
<CursorToysProvider>{children}</CursorToysProvider>
</body>
</html>
🤖 Prompt for AI Agents
In src/app/layout.tsx around lines 12 to 16, the root <html> element is missing
a lang attribute which harms accessibility and SEO; add a lang prop to the html
tag (use the current locale if your app supports per-locale routing, otherwise
default to "en") and ensure the locale value is read/passed in safely (e.g.,
from next/router, request, or a server prop) so the rendered <html lang="...">
reflects the active language.

function Button({ className, variant, size, ...props }: ButtonProps) {
return (
<ButtonPrimitive
className={cn(buttonVariants({ variant, size, className }))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

className isn’t merged; external classes are dropped.

You’re passing className into CVA options, so it’s ignored. Merge it via cn after resolving variants.

-      className={cn(buttonVariants({ variant, size, className }))}
+      className={cn(buttonVariants({ variant, size }), className)}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
className={cn(buttonVariants({ variant, size, className }))}
className={cn(buttonVariants({ variant, size }), className)}
🤖 Prompt for AI Agents
In src/components/animate-ui/components/buttons/button.tsx around line 51, the
external className is being passed into the CVA options and thus ignored; update
the render to call the variant resolver without className and then merge the
result with the external className using cn (e.g., cn(buttonVariants({ variant,
size }), className)) so external classes are preserved and merged correctly.

Comment on lines +433 to +446
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
delay,
}}
>
{content}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Bug: persistOnAnimateEnd not propagated via context

persistOnAnimateEnd is part of AnimateIconContextValue but is not provided here, breaking inheritance in nested IconWrapper/AnimateIcon trees.

   return (
     <AnimateIconContext.Provider
       value={{
         controls,
         animation: currentAnimation,
         loop,
         loopDelay,
         active: localAnimate,
         animate,
         initialOnAnimateEnd,
         completeOnStop,
+        persistOnAnimateEnd,
         delay,
       }}
     >
       {content}
     </AnimateIconContext.Provider>
   );
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
delay,
}}
>
{content}
<AnimateIconContext.Provider
value={{
controls,
animation: currentAnimation,
loop,
loopDelay,
active: localAnimate,
animate,
initialOnAnimateEnd,
completeOnStop,
persistOnAnimateEnd,
delay,
}}
>
{content}
</AnimateIconContext.Provider>
🤖 Prompt for AI Agents
In src/components/animate-ui/icons/icon.tsx around lines 433 to 446, the
AnimateIconContext.Provider value omits persistOnAnimateEnd which is declared on
the AnimateIconContextValue type and needed for nested IconWrapper/AnimateIcon
inheritance; add persistOnAnimateEnd to the provided context object (e.g.
include persistOnAnimateEnd: persistOnAnimateEnd) so the context propagates this
flag, and ensure the variable is defined in the surrounding scope and matches
the expected type.

Comment on lines +545 to +569
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Also add persistOnAnimateEnd in the IconWrapper provider

Without this, children can’t inherit the parent’s persistence behaviour.

   return (
     <AnimateIconContext.Provider
       value={{
         controls,
         animation: animationToUse,
         loop: loopToUse,
         loopDelay: loopDelayToUse,
         active: parentActive,
         animate: parentAnimate,
         initialOnAnimateEnd: parentInitialOnAnimateEnd,
+        persistOnAnimateEnd: parentPersistOnAnimateEnd,
         delay: parentDelay,
         completeOnStop: parentCompleteOnStop,
       }}
     >
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
return (
<AnimateIconContext.Provider
value={{
controls,
animation: animationToUse,
loop: loopToUse,
loopDelay: loopDelayToUse,
active: parentActive,
animate: parentAnimate,
initialOnAnimateEnd: parentInitialOnAnimateEnd,
persistOnAnimateEnd: parentPersistOnAnimateEnd,
delay: parentDelay,
completeOnStop: parentCompleteOnStop,
}}
>
<IconComponent
size={size}
className={cn(
className,
(animationToUse === "path" || animationToUse === "path-loop") &&
pathClassName,
)}
{...props}
/>
</AnimateIconContext.Provider>
);
🤖 Prompt for AI Agents
In src/components/animate-ui/icons/icon.tsx around lines 545 to 569, the
AnimateIconContext.Provider value is missing the persistOnAnimateEnd property so
children can't inherit the parent's persistence behavior; add
persistOnAnimateEnd: parentPersistOnAnimateEnd (using the existing
parentPersistOnAnimateEnd variable) to the value object so the provider forwards
that flag to children.

Comment on lines +105 to +133
function getWebGLContext(canvas: HTMLCanvasElement) {
const params = {
alpha: true,
depth: false,
stencil: false,
antialias: false,
preserveDrawingBuffer: false,
};

let gl = canvas.getContext(
"webgl2",
params,
) as WebGL2RenderingContext | null;

if (!gl) {
gl = (canvas.getContext("webgl", params) ||
canvas.getContext(
"experimental-webgl",
params,
)) as WebGL2RenderingContext | null;
}

if (!gl) {
throw new Error("Unable to initialize WebGL.");
}

const isWebGL2 = "drawBuffers" in gl;

let supportLinearFiltering = false;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

WebGL2 detection and types are unsafe; may misclassify WebGL1 with extensions.

Type gl as WebGLRenderingContext | WebGL2RenderingContext and detect WebGL2 via instanceof, not "drawBuffers" in gl.

Apply this diff:

@@
-      let gl = canvas.getContext(
-        "webgl2",
-        params,
-      ) as WebGL2RenderingContext | null;
+      let gl: WebGL2RenderingContext | WebGLRenderingContext | null =
+        canvas.getContext("webgl2", params) as WebGL2RenderingContext | null;
@@
-        gl = (canvas.getContext("webgl", params) ||
-          canvas.getContext(
-            "experimental-webgl",
-            params,
-          )) as WebGL2RenderingContext | null;
+        gl =
+          (canvas.getContext("webgl", params) as WebGLRenderingContext) ||
+          (canvas.getContext("experimental-webgl", params) as WebGLRenderingContext) ||
+          null;
@@
-      const isWebGL2 = "drawBuffers" in gl;
+      const isWebGL2 =
+        typeof WebGL2RenderingContext !== "undefined" &&
+        gl instanceof WebGL2RenderingContext;
@@
-      if (isWebGL2) {
+      if (isWebGL2) {
         (gl as WebGL2RenderingContext).getExtension("EXT_color_buffer_float");
         supportLinearFiltering = !!(gl as WebGL2RenderingContext).getExtension(
           "OES_texture_float_linear",
         );
       } else {
         halfFloat = gl.getExtension("OES_texture_half_float");
         supportLinearFiltering = !!gl.getExtension(
           "OES_texture_half_float_linear",
         );
       }

Also applies to: 119-126, 131-146

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 105 to 133 (and also apply same
changes to 119-126 and 131-146), the code unsafely types gl as
WebGL2RenderingContext and detects WebGL2 by checking "drawBuffers" in gl;
instead declare gl as WebGLRenderingContext | WebGL2RenderingContext, determine
WebGL2 by using instanceof WebGL2RenderingContext, and for WebGL1 use
getExtension("WEBGL_draw_buffers") (and others) to detect optional features;
update any subsequent feature checks (like supportLinearFiltering and draw
buffer support) to branch based on the instanceof result and extension
availability so WebGL1 with extensions is handled correctly.

Comment on lines +278 to +292
function compileShader(
type: number,
source: string,
keywords: string[] | null = null,
): WebGLShader | null {
const shaderSource = addKeywords(source, keywords);
const shader = gl.createShader(type);
if (!shader) return null;
gl.shaderSource(shader, shaderSource);
gl.compileShader(shader);
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.trace(gl.getShaderInfoLog(shader));
}
return shader;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Shader/program error handling continues with invalid objects.

Return null on failure and delete broken resources to avoid silent no‑ops later.

Apply this diff:

@@
-      gl.compileShader(shader);
-      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
-        console.trace(gl.getShaderInfoLog(shader));
-      }
-      return shader;
+      gl.compileShader(shader);
+      if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
+        console.trace(gl.getShaderInfoLog(shader) || "Shader compile error");
+        gl.deleteShader(shader);
+        return null;
+      }
+      return shader;
@@
-      gl.linkProgram(program);
-      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
-        console.trace(gl.getProgramInfoLog(program));
-      }
-      return program;
+      gl.linkProgram(program);
+      if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
+        console.trace(gl.getProgramInfoLog(program) || "Program link error");
+        gl.deleteProgram(program);
+        return null;
+      }
+      return program;

Also applies to: 294-308

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 278-292 and also lines 294-308,
the shader and program creation flow currently logs compile/link errors but
continues using broken objects; change the error handling to clean up and return
null: when compileShader detects a compile failure, call gl.deleteShader(shader)
before returning null (also handle createShader returning null), and for program
creation/linking, if createProgram returns null or linking fails, detach and
delete any attached shaders and delete the program, log the programInfoLog, and
then return null so callers don't use invalid WebGLProgram objects.

Comment on lines +294 to +308
function createProgram(
vertexShader: WebGLShader | null,
fragmentShader: WebGLShader | null,
): WebGLProgram | null {
if (!vertexShader || !fragmentShader) return null;
const program = gl.createProgram();
if (!program) return null;
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.trace(gl.getProgramInfoLog(program));
}
return program;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Vertex attribute not reliably bound; aPosition may not be at index 0.

Hardcoding location 0 in blit is undefined across programs. Bind attribute location 0 before linking.

Apply this diff:

@@
-      gl.attachShader(program, vertexShader);
-      gl.attachShader(program, fragmentShader);
-      gl.linkProgram(program);
+      gl.attachShader(program, vertexShader);
+      gl.attachShader(program, fragmentShader);
+      // Ensure attribute 0 is 'aPosition' for all programs to match blit() setup.
+      gl.bindAttribLocation(program, 0, "aPosition");
+      gl.linkProgram(program);
@@
-      gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
-      gl.enableVertexAttribArray(0);
+      gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
+      gl.enableVertexAttribArray(0);

Also applies to: 684-701

🤖 Prompt for AI Agents
In src/components/SplashCursor.tsx around lines 294-308 (and similarly around
684-701), the program links without binding attribute locations so the aPosition
attribute may not be at index 0; before calling gl.linkProgram(program) call
gl.bindAttribLocation(program, 0, "aPosition") (and any other fixed attribute
names you rely on) to force the attribute to location 0, then link and check the
link status as before.

Comment on lines +88 to +112
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
window.addEventListener("mousemove", moveHandler);

const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;

const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;

const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);

if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};

window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Duplicate mousemove listener; one remains after cleanup.

You add moveHandler twice (Lines 89 and 111) but remove it once. Remove the duplicate add.

-    window.addEventListener("mousemove", moveHandler);
@@
-    window.addEventListener("scroll", scrollHandler, { passive: true });
-    window.addEventListener("mousemove", moveHandler);
+    window.addEventListener("scroll", scrollHandler, { passive: true });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
window.addEventListener("mousemove", moveHandler);
const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;
const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;
const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);
if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);
const moveHandler = (e: MouseEvent) => moveCursor(e.clientX, e.clientY);
const scrollHandler = () => {
if (!activeTarget || !cursorRef.current) return;
const mouseX = gsap.getProperty(cursorRef.current, "x") as number;
const mouseY = gsap.getProperty(cursorRef.current, "y") as number;
const elementUnderMouse = document.elementFromPoint(mouseX, mouseY);
const isStillOverTarget =
elementUnderMouse &&
(elementUnderMouse === activeTarget ||
elementUnderMouse.closest(targetSelector) === activeTarget);
if (!isStillOverTarget) {
if (currentLeaveHandler) {
currentLeaveHandler();
}
}
};
window.addEventListener("scroll", scrollHandler, { passive: true });
window.addEventListener("mousemove", moveHandler);
🤖 Prompt for AI Agents
In src/components/TargetCursor.tsx around lines 88 to 112 there is a duplicate
window.addEventListener("mousemove", moveHandler) call (added at lines ~89 and
~111) which causes one listener to remain after cleanup; remove the
second/duplicate addEventListener call so moveHandler is only registered once
(and verify the existing cleanup/removeEventListener matches that single
registration).

Comment on lines +8 to +19
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function useControlledState<T, Rest extends any[] = []>(
props: CommonControlledStateProps<T> & {
onChange?: (value: T, ...args: Rest) => void;
},
): readonly [T, (next: T, ...args: Rest) => void] {
const { value, defaultValue, onChange } = props;

const [state, setInternalState] = React.useState<T>(
value !== undefined ? value : (defaultValue as T),
);

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid unsafe initial cast; allow undefined state and tighten generics

Current typing hides an undefined initial state behind a cast, risking runtime issues for consumers expecting T to be always defined.

-// eslint-disable-next-line @typescript-eslint/no-explicit-any
-export function useControlledState<T, Rest extends any[] = []>(
+export function useControlledState<T, Rest extends unknown[] = []>(
   props: CommonControlledStateProps<T> & {
     onChange?: (value: T, ...args: Rest) => void;
   },
-): readonly [T, (next: T, ...args: Rest) => void] {
+): readonly [T | undefined, (next: T, ...args: Rest) => void] {
   const { value, defaultValue, onChange } = props;
 
-  const [state, setInternalState] = React.useState<T>(
-    value !== undefined ? value : (defaultValue as T),
-  );
+  const [state, setInternalState] = React.useState<T | undefined>(
+    value !== undefined ? value : defaultValue,
+  );
 
   React.useEffect(() => {
-    if (value !== undefined) setInternalState(value);
+    if (value !== undefined) setInternalState(value);
   }, [value]);

Optional: add dev-time warning if the hook switches between controlled and uncontrolled during its lifetime.

Also applies to: 24-33

🤖 Prompt for AI Agents
In src/hooks/use-controlled-state.tsx around lines 8 to 19 (and similarly 24 to
33), the hook unsafely casts defaultValue to T which hides an undefined initial
state; change the state type to T | undefined, initialize useState with value
!== undefined ? value : defaultValue (no cast) so the initial state can be
undefined safely, tighten generics so the hook returns readonly [T | undefined,
(next: T | undefined, ...args: Rest) => void], and (optionally) add a dev-only
useRef + useEffect check that detects and warns if the hook switches between
controlled and uncontrolled during its lifecycle.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (7)
components.json (1)

14-20: The hooks alias still points to a non-existent directory.

This is identical to the critical issue flagged in the previous review: line 19 defines "hooks": "@/hooks" which references src/hooks, but this directory does not exist in the project structure. This will cause import failures at runtime if any code attempts to use imports like import { useSomething } from '@/hooks'.

Either create the src/hooks directory with appropriate hook files, or remove the "hooks" alias definition from the configuration.

To resolve this, please either:

Option 1: Create the missing directory:

#!/bin/bash
mkdir -p src/hooks
touch src/hooks/index.ts

Option 2: Remove the hooks alias from the configuration if it's not needed:

   "aliases": {
     "components": "@/components",
     "utils": "@/lib/utils",
     "ui": "@/components/ui",
     "lib": "@/lib",
-    "hooks": "@/hooks"
   },

Please confirm which approach you'd like to take and verify that the src/hooks directory exists after making changes.

tsconfig.json (1)

7-7: Disabling strict mode represents a type-safety regression (duplicate concern from past review).

Setting "strict": false disables all strict TypeScript checks and remains problematic during this major refactoring. Whilst the prior review flagged this same issue, it appears unresolved in this iteration.

Recommend reverting to "strict": true and addressing type errors that emerge, rather than suppressing type-checking altogether. This is especially important given the substantial changes to dependencies and React 19 compatibility.

src/app/[locale]/template.tsx (1)

8-8: Critical: Analytics still not gated behind user consent

This issue was flagged in a previous review and remains unresolved. The analytics hook is called unconditionally, violating privacy regulations. You must check consent before initialising Swetrix.

Apply this diff to gate analytics behind consent:

 "use client";
 import { useSwetrix } from "@swetrix/nextjs";
-import { ReactNode } from "react";
+import { ReactNode, useEffect } from "react";
+import { useConsentManager } from "@c15t/react";
 import { CursorToys } from "@/components/CursorToys";
 import SettingsController from "@/components/SettingsController";

 export default function PagesLayout({ children }: { children: ReactNode }) {
-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+  const { hasConsented } = useConsentManager();
+  
+  useEffect(() => {
+    if (hasConsented()) {
+      useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+    }
+  }, [hasConsented]);

   return (

Note: If useSwetrix cannot be called conditionally inside useEffect due to React hooks rules, you may need to use a conditional component pattern or check Swetrix documentation for a disable/enable configuration option.

src/components/vrm.tsx (3)

129-218: This segment still lacks resource cleanup and unmount protection.

The critical issue regarding unmount state updates and resource leaks identified in the previous review remains unresolved.


232-235: Verify security attribute on external link.

A previous review identified that this anchor is missing rel="noopener noreferrer" and marked it as addressed. However, the current code still shows the attribute is absent, suggesting either a regression or outdated code view.


249-277: Interactive icons still need accessibility improvements.

The accessibility issues with non-button interactive icons identified in the previous review remain unaddressed.

src/app/[locale]/page.tsx (1)

19-22: Params signature already flagged in previous review.

A previous review comment identified that this params signature may be incorrect for the Next.js version in use. Please address the existing feedback about whether params should be a Promise or a synchronous object.

🧹 Nitpick comments (4)
src/lib/src/lib/utils.ts (1)

4-6: LGTM!

The implementation correctly follows the standard pattern for Tailwind class composition. The function pipes inputs through clsx for normalisation and then twMerge for intelligent Tailwind class deduplication.

Optionally, consider adding JSDoc documentation to describe the utility's purpose and usage for better developer experience.

src/components/ui/src/components/ui/button.tsx (2)

14-15: Consider using a semantic colour token for consistency.

The destructive variant uses text-white whilst other variants use semantic tokens (e.g., text-primary-foreground). For consistency and maintainability, consider using text-destructive-foreground if it's defined in your theme.

Apply this diff to use a semantic token:

       destructive:
-        "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
+        "bg-destructive text-destructive-foreground shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",

44-47: Consider exporting ButtonProps type.

The inline type definition is fine, but exporting it as ButtonProps would improve reusability for consumers who need to type-check their props or extend the button.

Apply this diff to export the type:

+export type ButtonProps = React.ComponentProps<"button"> &
+  VariantProps<typeof buttonVariants> & {
+    asChild?: boolean;
+  };
+
-function Button({
+function Button({
   className,
   variant,
   size,
   asChild = false,
   ...props
-}: React.ComponentProps<"button"> &
-  VariantProps<typeof buttonVariants> & {
-    asChild?: boolean;
-  }) {
+}: ButtonProps) {
src/components/BubbleMenu.tsx (1)

186-237: Consider extracting inline styles.

The embedded <style> block works but may be recreated on every render. For better maintainability and performance, consider moving these styles to a CSS module or global stylesheet.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4a06599 and 5636047.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (16)
  • components.json (1 hunks)
  • package.json (1 hunks)
  • src/app/[locale]/page.tsx (2 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/components/BubbleMenu.tsx (1 hunks)
  • src/components/CustomCursor.tsx (1 hunks)
  • src/components/SettingsController.tsx (1 hunks)
  • src/components/SpotlightCard.tsx (1 hunks)
  • src/components/fancy/blocks/marquee-along-svg-path.tsx (1 hunks)
  • src/components/fancy/blocks/simple-marquee.tsx (1 hunks)
  • src/components/fancy/text/basic-number-ticker.tsx (1 hunks)
  • src/components/ui/src/components/ui/button.tsx (1 hunks)
  • src/components/vrm.tsx (1 hunks)
  • src/lib/src/lib/utils.ts (1 hunks)
  • src/messages/en.json (1 hunks)
  • tsconfig.json (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/components/SettingsController.tsx
  • src/messages/en.json
  • src/components/CustomCursor.tsx
🧰 Additional context used
🧬 Code graph analysis (6)
src/components/ui/src/components/ui/button.tsx (1)
src/lib/src/lib/utils.ts (1)
  • cn (4-6)
src/components/fancy/blocks/marquee-along-svg-path.tsx (1)
src/lib/src/lib/utils.ts (1)
  • cn (4-6)
src/app/[locale]/template.tsx (2)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/SettingsController.tsx (1)
  • SettingsController (34-114)
src/components/fancy/blocks/simple-marquee.tsx (1)
src/lib/src/lib/utils.ts (1)
  • cn (4-6)
src/components/fancy/text/basic-number-ticker.tsx (1)
src/lib/src/lib/utils.ts (1)
  • cn (4-6)
src/app/[locale]/page.tsx (1)
src/components/vrm.tsx (1)
  • VRM (96-281)
🔇 Additional comments (21)
components.json (1)

6-12: No issues found—configuration files are present and correctly referenced.

Both tailwind.config.ts and src/app/globals.css exist at their specified locations. The Tailwind configuration in components.json is valid and will not encounter file-not-found errors at build time.

src/lib/src/lib/utils.ts (1)

1-2: LGTM!

The imports are correct and appropriate for a Tailwind class composition utility.

tsconfig.json (1)

15-15: JSX runtime modernisation looks good.

The change from "preserve" to "react-jsx" aligns well with React 19 and modern Next.js tooling. This allows automatic JSX transformation without explicit React imports, which is the recommended approach.

src/components/ui/src/components/ui/button.tsx (2)

38-57: Well-implemented button component with good accessibility features.

The component correctly implements:

  • Polymorphic rendering via Radix Slot
  • Proper prop forwarding and className merging
  • Accessibility features (focus-visible, aria-invalid states)
  • Type-safe variant system

5-5: Import path is correct—no changes required.

The import path @/lib/utils resolves correctly via your tsconfig path mapping (@/*./src/*), resulting in the canonical file src/lib/utils.ts. The nested src/lib/src/lib/utils.ts is a separate artefact that does not interfere with module resolution.

src/components/vrm.tsx (4)

15-42: Animation configuration looks good.

The weighted random selection approach with percentages is clear and the data structure is well-defined.


44-94: VRMModel implementation is sound.

The conditional eye tracking, smooth head rotation interpolation, and graceful error handling for the lookAt feature are well implemented.


107-127: Mouse tracking effect is correctly implemented.

The event listener is properly cleaned up and the normalized coordinate calculation is appropriate.


236-244: Canvas setup is appropriate.

The camera positioning, lighting, and prop wiring to VRMModel are all correct.

src/components/SpotlightCard.tsx (2)

1-12: LGTM on type definitions.

The interfaces are well-structured. The template literal type for spotlightColor ensures type safety for rgba values.


14-48: Well-implemented state and event handlers.

The mouse tracking logic correctly computes relative coordinates, and the focus state prevents mouse interference during keyboard navigation.

src/components/fancy/text/basic-number-ticker.tsx (2)

1-34: LGTM on interfaces and imports.

The prop and ref interfaces are well-documented with inline comments, making the API clear for consumers.


88-98: LGTM on render and export.

The component correctly renders the animated value using motion.span, and the display name aids debugging. The usage example in comments is helpful for consumers.

src/app/[locale]/page.tsx (4)

70-122: LGTM on info section structure.

The OSS and affordability sections are well-structured with proper image alt text and responsive styling. The GitHub link provides transparency.


194-198: Verify 96% uptime target is intentional.

The displayed monthly uptime is 96%, which is lower than typical production standards (usually 99%+). If this is actual data, consider whether this meets user expectations; if it's a placeholder, update to a more realistic target.


211-300: Page structure and partner showcase look good.

The self-funded messaging, partner logos with hover effects, and final CTA section are well-organized. The empty spacer div (line 284) provides visual breathing room before the final section.


24-68: All translation keys verified in locale files.

The hero section translation keys (creating-cool, makeLifeEasier, mainBlurb1, mainBlurb2, takeLook, learnMore) have been confirmed to exist in both src/messages/en.json and src/messages/ja.json. No runtime translation errors will occur.

src/components/BubbleMenu.tsx (3)

386-403: Verify height: 10 is intentional.

Line 402 sets height: 10 (defaults to 10px), which seems inconsistent with the minHeight of 160px and generous padding. This may cause layout issues or be overridden by other styles.

If this is intentional, consider adding a comment explaining the purpose. Otherwise, review whether this should be removed or use a different value.


239-425: LGTM: Solid accessibility implementation.

The component uses appropriate ARIA roles (menu, menuitem, none) and attributes (aria-label, aria-pressed, aria-hidden), making it keyboard-navigable and screen reader-friendly.


112-163: LGTM: Well-structured animation logic.

The GSAP animations properly handle cleanup (killTweensOf), set initial states, and include appropriate callbacks. The dependency array correctly lists all external values used within the effect.

package.json (1)

36-36: The review comment is incorrect — "radix-ui" is a legitimate official package.

The unscoped "radix-ui" package exists on npm and is the official Radix UI package (version 1.4.3). It's not a typo or third-party alternative. The codebase actively uses it: src/components/animate-ui/primitives/radix/radio-group.tsx imports RadioGroup directly from "radix-ui", confirming this dependency is intentional.

Likely an incorrect or invalid review comment.

"@biomejs/biome": "^2.3.1",
"@types/node": "^20",
"@types/react": "^19.2.2",
"@types/react-dom": "^18",
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Update React DOM types to match React DOM 19.

The project uses react-dom 19.2.0 (line 38) but @types/react-dom ^18. This mismatch can cause type errors with React 19 DOM APIs.

Apply this diff:

-    "@types/react-dom": "^18",
+    "@types/react-dom": "^19",

Then regenerate your lockfile and run the typechecker to verify compatibility.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"@types/react-dom": "^18",
"@types/react-dom": "^19",
🤖 Prompt for AI Agents
In package.json around line 54, the devDependency "@types/react-dom": "^18"
conflicts with the project using "react-dom" 19.2.0; update the @types/react-dom
entry to a version that matches React 19 (e.g., "^19" or the appropriate 19.x
tag), then regenerate the lockfile (npm/yarn/pnpm install) and run the
TypeScript typechecker to verify compatibility and fix any type issues.

Comment on lines +47 to +52
label: "projects",
href: "#",
ariaLabel: "Documentation",
rotation: 8,
hoverStyles: { bgColor: "#f59e0b", textColor: "#ffffff" },
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix ariaLabel mismatch.

The ariaLabel reads "Documentation" whilst the visible label is "projects". This inconsistency could confuse screen reader users.

Apply this diff to align the labels:

   {
     label: "projects",
     href: "#",
-    ariaLabel: "Documentation",
+    ariaLabel: "Projects",
     rotation: 8,
     hoverStyles: { bgColor: "#f59e0b", textColor: "#ffffff" },
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
label: "projects",
href: "#",
ariaLabel: "Documentation",
rotation: 8,
hoverStyles: { bgColor: "#f59e0b", textColor: "#ffffff" },
},
label: "projects",
href: "#",
ariaLabel: "Projects",
rotation: 8,
hoverStyles: { bgColor: "#f59e0b", textColor: "#ffffff" },
},
🤖 Prompt for AI Agents
In src/components/BubbleMenu.tsx around lines 47 to 52 the ariaLabel is
"Documentation" while the visible label is "projects", causing an accessibility
mismatch; update the ariaLabel to match the visible label (e.g., "projects" or
"Projects" to follow your app's casing convention) so screen readers read the
same label as sighted users.

Comment on lines +346 to +404
{items.map(({ child, repeatIndex, itemIndex, key }) => {
// Create a unique offset transform for each item
const itemOffset = useTransform(baseOffset, (v) => {
const position = (itemIndex * 100) / items.length;
const wrappedValue = wrap(0, 100, v + position);
return `${easing ? easing(wrappedValue / 100) * 100 : wrappedValue}%`;
});

// Create a motion value for the current offset distance
const currentOffsetDistance = useMotionValue(0);

// Update z-index when offset distance changes
const zIndex = useTransform(currentOffsetDistance, (value) =>
calculateZIndex(value),
);

// Update current offset distance value when animation runs
useEffect(() => {
const unsubscribe = itemOffset.on("change", (value: string) => {
// Parse percentage string to get numerical value
const match = value.match(/^([\d.]+)%$/);
if (match && match[1]) {
currentOffsetDistance.set(parseFloat(match[1]));
}
});
return unsubscribe;
}, [itemOffset, currentOffsetDistance]);

const cssVariables = Object.fromEntries(
(cssVariableInterpolation || []).map(({ property, from, to }) => [
property,
useTransform(currentOffsetDistance, [0, 100], [from, to]),
]),
);

return (
<motion.div
key={key}
ref={(el) => {
if (el) itemRefs.current.set(key, el);
}}
className={cn(
"absolute top-0 left-0",
draggable && grabCursor && "cursor-grab",
)}
style={{
offsetPath: `path('${path}')`,
offsetDistance: itemOffset,
zIndex: enableRollingZIndex ? zIndex : undefined,
...cssVariables,
}}
aria-hidden={repeatIndex > 0}
onMouseEnter={() => (isHovered.current = true)}
onMouseLeave={() => (isHovered.current = false)}
>
{child}
</motion.div>
);
})}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Move per-item hooks out of the render loop.

Inside items.map(...) you call useTransform, useMotionValue, and useEffect. That violates the Rules of Hooks; the hook order changes whenever repeat or children changes, so React will throw (“Rendered more hooks than expected”). Extract these calculations into a dedicated child component (e.g. <MarqueeItem />) or a custom hook invoked once per item outside the loop so the hook order stays stable.

🤖 Prompt for AI Agents
In src/components/fancy/blocks/marquee-along-svg-path.tsx around lines 346-404,
hooks (useTransform, useMotionValue, useEffect) are being called inside
items.map which breaks the Rules of Hooks; extract the per-item logic into a
separate component (e.g. MarqueeItem) that receives the item props (child, key,
repeatIndex, itemIndex), baseOffset, items.length, easing,
cssVariableInterpolation, path, enableRollingZIndex, draggable, grabCursor,
isHovered ref, itemRefs, and calculateZIndex and performs the
useTransform/useMotionValue/useEffect calls there; then replace the map body
with a stable JSX call to <MarqueeItem ... /> for each item, forward the ref/key
properly, compute css variables inside the child with useTransform, and ensure
the parent no longer invokes hooks inside the render loop so hook order remains
stable.

Comment on lines +113 to +122
const x = useTransform(baseX, (v) => {
// Apply easing if provided, otherwise use linear (v directly)
const wrappedValue = wrap(0, -100, v);
return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`;
});
const y = useTransform(baseY, (v) => {
// Apply easing if provided, otherwise use linear (v directly)
const wrappedValue = wrap(0, -100, v);
return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`;
});
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix wrap range: current implementation inverts min/max.

Passing min=0 and max=-100 makes range = max - min negative, so even tiny offsets wrap to ~-100% immediately. The marquee leaps to the end of the loop instead of tracking smoothly. Swap the arguments so the range is positive.

-    const wrappedValue = wrap(0, -100, v);
+    const wrappedValue = wrap(-100, 0, v);

Apply the same fix to the y transform below.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/fancy/blocks/simple-marquee.tsx around lines 113 to 122, the
wrap() calls pass min=0 and max=-100 which inverts the range and causes
immediate wrapping; swap the arguments to wrap(-100, 0, v) for both x and y
transforms so the range is positive, and keep the rest of the easing/percentage
logic unchanged.

Comment on lines +61 to +74
const startAnimation = useCallback(() => {
if (controls) controls.stop();
onStart?.();

count.set(from);

const newControls = animate(count, target, {
...transition,
onComplete: () => {
onComplete?.();
},
});
setControls(newControls);
}, []);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Fix stale closure in startAnimation callback.

The useCallback has an empty dependency array but references controls, onStart, count, from, target, transition, and onComplete. This creates a stale closure—if props change, startAnimation will use outdated values.

Apply this diff to fix the dependencies:

     const startAnimation = useCallback(() => {
       if (controls) controls.stop();
       onStart?.();
 
       count.set(from);
 
       const newControls = animate(count, target, {
         ...transition,
         onComplete: () => {
           onComplete?.();
         },
       });
       setControls(newControls);
-    }, []);
+    }, [controls, onStart, count, from, target, transition, onComplete]);
🤖 Prompt for AI Agents
In src/components/fancy/text/basic-number-ticker.tsx around lines 61 to 74, the
startAnimation useCallback currently has an empty dependency array but closes
over controls, onStart, count, from, target, transition, and onComplete causing
stale values; update the useCallback dependency array to include these
references (controls, onStart, count, from, target, transition, onComplete) so
the callback captures current props/state, and keep the existing logic (guard
controls.stop(), call onStart, set count, start animate and setControls)
unchanged.

Comment on lines +81 to +86
useEffect(() => {
if (autoStart) {
startAnimation();
}
return () => controls?.stop();
}, [autoStart]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add missing dependencies to effect.

The effect calls startAnimation and references controls in cleanup but doesn't include them in the dependency array. This can cause the effect to use stale references.

Apply this diff:

     useEffect(() => {
       if (autoStart) {
         startAnimation();
       }
       return () => controls?.stop();
-    }, [autoStart]);
+    }, [autoStart, startAnimation, controls]);

Note: After fixing the startAnimation dependencies (previous comment), this will work correctly.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (autoStart) {
startAnimation();
}
return () => controls?.stop();
}, [autoStart]);
useEffect(() => {
if (autoStart) {
startAnimation();
}
return () => controls?.stop();
}, [autoStart, startAnimation, controls]);
🤖 Prompt for AI Agents
In src/components/fancy/text/basic-number-ticker.tsx around lines 81 to 86, the
useEffect references startAnimation and controls but only lists autoStart in the
dependency array; update the effect to include startAnimation and controls
(e.g., [autoStart, startAnimation, controls]) so it won't use stale references,
and ensure startAnimation is memoized (useCallback) as noted in the prior
comment so it remains stable for the dependency array to work correctly.

Comment on lines +50 to +68
<div
ref={divRef}
onMouseMove={handleMouseMove}
onFocus={handleFocus}
onBlur={handleBlur}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`relative rounded-3xl border border-neutral-800 bg-neutral-900 overflow-hidden p-8 ${className}`}
>
<div
className="pointer-events-none absolute inset-0 opacity-0 transition-opacity duration-500 ease-in-out"
style={{
opacity,
background: `radial-gradient(circle at ${position.x}px ${position.y}px, ${spotlightColor}, transparent 80%)`,
}}
/>
{children}
</div>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add keyboard accessibility attributes.

The onFocus and onBlur handlers won't fire because div elements aren't focusable by default. This prevents keyboard users from interacting with the spotlight effect.

Apply this diff to enable keyboard focus:

     <div
       ref={divRef}
       onMouseMove={handleMouseMove}
       onFocus={handleFocus}
       onBlur={handleBlur}
       onMouseEnter={handleMouseEnter}
       onMouseLeave={handleMouseLeave}
+      tabIndex={0}
       className={`relative rounded-3xl border border-neutral-800 bg-neutral-900 overflow-hidden p-8 ${className}`}
     >

If the card contains interactive elements (links, buttons), consider using tabIndex={-1} instead to avoid double-tabbing, or omit keyboard focus entirely if it's purely decorative.

🤖 Prompt for AI Agents
In src/components/SpotlightCard.tsx around lines 50 to 68 the root div is not
focusable so the onFocus/onBlur handlers won't fire; make the container
keyboard-focusable by adding tabIndex={0} to the div (or use tabIndex={-1}
instead if the card contains its own interactive elements to avoid
double-tabbing), keep the existing mouse and focus handlers intact, and ensure
you don't add focus styles that clash with current design.

Comment on lines +32 to +33
".next\\dev/types/**/*.ts",
".next\\dev/types/**/*.ts"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove duplicate and platform-specific path entries.

Lines 32–33 contain redundant entries for .next\dev/types/**/*.ts (using Windows-style backslashes). These duplicate the forward-slash entry on line 28 (.next/dev/types/**/*.ts).

The tsconfig.json format uses forward slashes across all platforms. Remove the duplicate Windows-style entries to keep the configuration clean and maintainable.

Apply this diff to remove the redundant entries:

  "include": [
    "next-env.d.ts",
    ".next/types/**/*.ts",
    ".next/dev/types/**/*.ts",
    "**/*.mts",
    "**/*.ts",
    "**/*.tsx",
-   ".next\\dev/types/**/*.ts",
-   ".next\\dev/types/**/*.ts"
  ],
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
".next\\dev/types/**/*.ts",
".next\\dev/types/**/*.ts"
"include": [
"next-env.d.ts",
".next/types/**/*.ts",
".next/dev/types/**/*.ts",
"**/*.mts",
"**/*.ts",
"**/*.tsx"
],
🤖 Prompt for AI Agents
In tsconfig.json around lines 32–33, remove the redundant Windows-style path
entries ".next\\dev/types/**/*.ts" which duplicate the existing forward-slash
entry ".next/dev/types/**/*.ts" (line ~28); edit the "include"/"exclude" array
to keep only the platform-neutral forward-slash paths and delete the duplicate
backslash entries so the config is clean and cross-platform.

# Conflicts:
#	components.json
#	src/app/[locale]/page.tsx
#	src/app/[locale]/template.tsx
#	src/components/SettingsController.tsx
#	src/components/vrm.tsx
#	src/messages/en.json
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (4)
package.json (1)

54-54: Update React DOM types to match React 19 (reoccurring issue).

Line 54 still declares @types/react-dom: ^18, which conflicts with react-dom 19.2.1 on line 39. This mismatch will cause type errors with React 19 DOM APIs. Although this was flagged in the previous review, it appears the fix was not applied or reverted.

Apply this diff to resolve the type mismatch:

-    "@types/react-dom": "^18",
+    "@types/react-dom": "^19",

Then regenerate your lockfile and run the TypeScript typechecker to verify compatibility.

src/app/layout.tsx (1)

14-14: Add lang attribute to <html> for accessibility and SEO.

The root <html> element lacks a language attribute, which harms accessibility for screen readers and SEO.

As noted in the previous review, apply:

-    <html className={notoSansJP.variable}>
+    <html lang="en" className={notoSansJP.variable}>

If you have per-locale routing, pass the locale dynamically instead of hardcoding.

src/app/[locale]/template.tsx (1)

17-17: Gate analytics behind user consent.

This issue was previously flagged: useSwetrix is called unconditionally on every render. Use useConsentManager from @c15t/react to check consent status before initialising analytics.

src/components/vrm.tsx (1)

85-168: Add cancellation and resource cleanup to prevent memory leaks and state updates after unmount.

This issue was previously flagged and remains unaddressed. The loaders can resolve after the component unmounts, leading to React state updates on unmounted components and WebGL resource leaks. Add a cancellation flag and proper cleanup using VRMUtils.deepDispose.

   useEffect(() => {
+    let cancelled = false;
+    let activeVrm: import("@pixiv/three-vrm").VRM | null = null;
+    let activeMixer: AnimationMixer | null = null;
     const loader = new GLTFLoader();
     loader.register((parser: any) => new VRMLoaderPlugin(parser));
     loader.register((parser: any) => new VRMAnimationLoaderPlugin(parser));

     const loadModel = () => {
       return new Promise<import("@pixiv/three-vrm").VRM>((resolve, reject) => {
         loader.load(
           "https://cdn.mikn.dev/vroid/mikan.dev(kyonyu).vrm",
           (gltf: GLTF) => {
             const loadedVrm = gltf.userData.vrm;
+            activeVrm = loadedVrm;
+            if (cancelled) return;
             setVrm(loadedVrm);
             resolve(loadedVrm);
           },
           // ... rest of loader
         );
       });
     };

     // In loadAnimation success callback:
+    activeMixer = animationMixer;
+    if (cancelled) {
+      action.stop();
+      return;
+    }

     loadModel()
       .then((loadedVrm) => {
+        if (cancelled) return;
         setIsLoaded(true);
         loadAnimation(loadedVrm);
       })
       .catch((error) => {
         console.error("An error occurred:", error);
       });
+
+    return () => {
+      cancelled = true;
+      try { actionRef.current?.stop(); } catch {}
+      try { activeMixer?.stopAllAction(); } catch {}
+      // Import VRMUtils from @pixiv/three-vrm for proper disposal
+      // try { if (activeVrm?.scene) VRMUtils.deepDispose(activeVrm.scene); } catch {}
+    };
   }, []);
🧹 Nitpick comments (12)
src/components/ui/shadcn-io/spinner/index.tsx (3)

1-9: Type reference before declaration is unconventional but functional.

SpinnerVariantProps on line 9 references SpinnerProps which is defined later on line 241. This works due to TypeScript's type hoisting, but for clarity, consider moving the SpinnerProps type definition before SpinnerVariantProps.


11-21: Pervasive as any casts reduce type safety.

The {...(props as any)} pattern is used throughout all variant components. Whilst this works, it bypasses TypeScript's type checking. Consider using a more precise type or creating a shared base props type that both Lucide icons and custom SVGs can accept.

If the goal is to pass through SVG attributes, you could use React.SVGProps<SVGSVGElement> for custom SVGs and keep the Lucide types separate:

-const Default = ({ className, ...props }: SpinnerVariantProps) => (
-  <LoaderIcon className={cn('animate-spin', className)} {...(props as any)} />
+const Default = ({ className, ...props }: SpinnerVariantProps) => (
+  <LoaderIcon className={cn('animate-spin', className)} {...props} />
);

This should work without casting since SpinnerVariantProps already extends LucideProps.


149-206: Global CSS class names in <style> tag risk style collisions.

The Bars variant injects global CSS with class names like .spinner-bar, .spinner-bars-2, .spinner-bars-3. If multiple Bars spinners render simultaneously or if these class names exist elsewhere, styles may conflict unexpectedly.

Consider using more unique class names or CSS Modules/scoped styles to avoid collisions:

    <style>{`
-      .spinner-bar {
+      .spinner-bar-${Math.random().toString(36).slice(2, 9)} {

Alternatively, use inline style attributes on the <rect> elements with CSS-in-JS, or leverage Tailwind's animation utilities if available.

src/components/mikn/Footer.tsx (1)

51-56: Consider adding rel attributes for external links.

If any footer links point to external domains, they should include rel="noopener noreferrer" for security and target="_blank" if intended to open in a new tab. If all links are internal, this can be ignored.

src/components/SplitText.tsx (2)

61-66: Empty catch blocks silently swallow errors.

Catching and ignoring exceptions makes debugging difficult. Consider logging in development or removing the try-catch if revert() is expected to throw safely.

       if (el._rbsplitInstance) {
         try {
           el._rbsplitInstance.revert();
-        } catch (_) {}
+        } catch (e) {
+          if (process.env.NODE_ENV === 'development') {
+            console.warn('SplitText revert failed:', e);
+          }
+        }
         el._rbsplitInstance = undefined;
       }

156-207: Consider using React.createElement to reduce repetition.

The switch statement repeats identical JSX for each tag. Using createElement would be more concise and maintainable.

-  const renderTag = () => {
-    const style: React.CSSProperties = { ... };
-    const classes = `...`;
-    switch (tag) {
-      case "h1":
-        return <h1 ref={ref} style={style} className={classes}>{text}</h1>;
-      // ... repeated for each tag
-    }
-  };
+  const Tag = tag;
+  const style: React.CSSProperties = {
+    textAlign,
+    wordWrap: "break-word",
+    willChange: "transform, opacity",
+  };
+  const classes = `split-parent overflow-hidden inline-block whitespace-normal ${className}`;
+
+  return (
+    <Tag ref={ref as React.RefObject<HTMLParagraphElement>} style={style} className={classes}>
+      {text}
+    </Tag>
+  );

Note: The ref cast is needed due to TypeScript limitations with dynamic tags.

src/app/[locale]/template.tsx (2)

20-73: Consider memoising or hoisting static data arrays.

The social and links arrays are recreated on every render. Since they only depend on translations, consider using useMemo or extracting the structure outside the component and only applying translations inside.

+const socialTemplate = [
+  {
+    name: "GitHub",
+    href: "https://github.com/mikndotdev",
+    color: "hover:text-github hover:bg-github",
+    icon: Github,
+  },
+];
+
 export default function PagesLayout({ children }: { children: ReactNode }) {
   useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
   const t = useTranslations("layout");

-  const social = [
-    {
-      name: "GitHub",
-      href: "https://github.com/mikndotdev",
-      color: "hover:text-github hover:bg-github",
-      icon: Github,
-    },
-  ];
+  const social = socialTemplate;

96-102: Using .src on Next.js static imports is unconventional.

When using Next.js Image with a statically imported asset, you can pass the import directly without accessing .src. The current approach works but bypasses some optimisation metadata.

           <Image
-            src={MikanCat.src}
+            src={MikanCat}
             width={200}
             height={100}
             alt=":3"
             className="ml-2 mb-0"
           />
src/components/mikn/Header.tsx (3)

112-142: Remove unused index prop from MobileMenuItem.

The index prop is declared in the interface and destructured in the component signature but never used within the component body. This is dead code that should be cleaned up.

 interface MobileMenuItemProps {
   name: string;
   href: string;
-  index: number;
   isCurrent: boolean;
   color: string;
 }

 const MobileMenuItem: React.FC<MobileMenuItemProps> = ({
   name,
   href,
   color,
   isCurrent,
 }) => {

Also update the call site at line 308:

                   <MobileMenuItem
                     key={item.name}
                     name={item.name}
                     href={item.href}
-                    index={index}
                     color={_color}
                     isCurrent={isCurrent}
                   />

277-281: Consider using Next.js Image for optimised image loading.

Using a native <img> tag bypasses Next.js image optimisation. If the logo source is known at build time (as it appears to be from Logo.src), consider using the Image component for better performance.


298-312: Using item.name as a React key may cause issues with duplicate names.

If navigation items have duplicate names, React will encounter key collisions. Consider using a more stable unique identifier or the index combined with the name.

-                   key={item.name}
+                   key={`${index}-${item.name}`}

Also applies to: 361-398

src/app/[locale]/page.tsx (1)

44-58: Hardcoded CircularText size may cause layout issues on smaller screens.

The size={400} and radius={160} values are fixed, but the container uses responsive classes (max-w-sm md:max-w-md). On smaller viewports, the CircularText may overflow or clip. Consider making these values responsive or ensuring the container properly constrains the content.

Test on mobile viewports to ensure the circular text doesn't overflow or cause horizontal scrolling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5636047 and e0256fc.

⛔ Files ignored due to path filters (2)
  • bun.lock is excluded by !**/*.lock
  • src/assets/fonts/HSR.woff2 is excluded by !**/*.woff2
📒 Files selected for processing (16)
  • components.json (1 hunks)
  • next.config.mjs (1 hunks)
  • package.json (1 hunks)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/layout.tsx (1 hunks)
  • src/app/not-found.tsx (1 hunks)
  • src/components/CircularText.tsx (1 hunks)
  • src/components/SettingsController.tsx (1 hunks)
  • src/components/SplitText.tsx (1 hunks)
  • src/components/mikn/Footer.tsx (1 hunks)
  • src/components/mikn/Header.tsx (1 hunks)
  • src/components/ui/shadcn-io/spinner/index.tsx (1 hunks)
  • src/components/vrm.tsx (1 hunks)
  • src/messages/en.json (1 hunks)
  • src/proxy.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (4)
  • next.config.mjs
  • src/app/not-found.tsx
  • src/components/SettingsController.tsx
  • components.json
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/[locale]/template.tsx (4)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/mikn/Header.tsx (1)
  • Header (167-433)
src/components/SettingsController.tsx (1)
  • SettingsController (34-95)
src/components/mikn/Footer.tsx (1)
  • Footer (23-87)
src/app/layout.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • CursorToysProvider (15-37)
src/components/vrm.tsx (1)
src/components/ui/shadcn-io/spinner/index.tsx (1)
  • Spinner (253-272)
src/app/[locale]/page.tsx (1)
src/components/vrm.tsx (1)
  • VRM (78-204)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (9)
src/components/ui/shadcn-io/spinner/index.tsx (2)

253-272: LGTM – switch-based variant selection is clear and maintainable.

The switch statement cleanly maps variant strings to their respective components with a sensible default fallback. The explicit default case ensures the component always renders something.


44-88: Good accessibility practice with <title> elements.

All custom SVG variants include <title>Loading...</title> which provides accessible names for screen readers. This is a solid accessibility pattern.

Also applies to: 90-147, 208-239

src/proxy.ts (1)

7-7: No action needed. The matcher pattern is correct for this configuration.

The routing configuration uses shared pathnames with locales ["en", "ja"] detected at middleware level, not URL-prefixed routes. Paths like /en/api or /fr/img do not occur in this setup—locale detection happens via request context, not URL segments. The matcher correctly excludes /api, /img, /_next, /_vercel, and static files from the i18n middleware, and all other routes are processed with the appropriate locale context automatically.

src/components/CircularText.tsx (1)

125-128: Component is invisible by default until startAnimation is triggered.

With initial={{ rotate: 0, scale: 0, opacity: 0 }} and startAnimation defaulting to false, the component remains invisible until startAnimation becomes true. Ensure consumers are aware of this requirement.

src/components/SplitText.tsx (1)

139-151: Dependency array includes onLetterAnimationComplete which may cause re-renders.

If consumers pass an inline arrow function as onLetterAnimationComplete, the animation will restart on every parent render. Consider documenting that the callback should be memoised, or wrap it internally.

Verify usage in consuming components to ensure callbacks are memoised with useCallback.

src/app/layout.tsx (1)

16-16: No configuration change needed — messages are provided at the localized layout level.

The NextIntlClientProvider in the root layout (src/app/layout.tsx) intentionally omits the messages prop because this project follows the correct next-intl App Router pattern: the root provider initializes the context, and the nested localized layout (src/app/[locale]/layout.tsx) provides the actual messages via getMessages() at line 43. Client components receive translations through the nested provider, so no additional configuration is required.

src/components/mikn/Header.tsx (1)

217-222: Potential stale closure issue with lastYPosition.

The useMotionValueEvent callback captures lastYPosition from the render scope, but this value may be stale by the time the callback executes. Consider using a ref to store the last position, or restructure to avoid the dependency.

+  const lastYPositionRef = useRef(0);
   const [isScrolled, setIsScrolled] = useState(false);
-  const [lastYPosition, setLastYPosition] = useState(0);
   const [isHeaderShown, setIsHeaderShown] = useState(true);

   useMotionValueEvent(scrollY, "change", (latest) => {
     setIsScrolled(latest > 10);
     if (!isMobileMenuOpen) {
-      setIsHeaderShown(latest < headerHeight || latest < lastYPosition);
-      setLastYPosition(latest);
+      setIsHeaderShown(latest < headerHeight || latest < lastYPositionRef.current);
+      lastYPositionRef.current = latest;
     }
   });
src/app/[locale]/page.tsx (1)

1-11: LGTM - Client component approach addresses previous concerns.

The page is now a client component using "use client", which resolves the previous issues about incorrect async page props signature. The useTranslations hook is the correct approach for client-side translations.

src/components/vrm.tsx (1)

181-203: LGTM - Render structure and Link usage are correct.

The conditional rendering with the Spinner loading state and the Link wrapper around the Canvas are well-implemented. Next.js Link component handles security attributes automatically, addressing the previous concern about target="_blank".

Comment on lines +54 to +69
const rotation: MotionValue<number> = useMotionValue(0);

useEffect(() => {
if (!startAnimation) return;

const start = rotation.get();
controls.start({
rotate: start + 360,
scale: 1,
opacity: 1,
transition: {
...getTransition(spinDuration, start),
opacity: { duration: 0.5 },
},
});
}, [startAnimation, spinDuration, text, onHover, controls, rotation]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

The rotation MotionValue may not track actual rotation correctly.

The rotation MotionValue is created but never updated by the animation. When rotation.get() is called in hover handlers, it will always return 0 (the initial value) rather than the current animated rotation, causing the animation to restart from 0 on each hover.

To fix this, connect the MotionValue to the animation output:

-      rotate: start + 360,
+      rotate: [start, start + 360],

Or use onUpdate to sync the rotation value, or consider using useMotionValueEvent to track the animated rotation state.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/CircularText.tsx around lines 54 to 69, the rotation
MotionValue is created but never driven by the animation so rotation.get() stays
0; update the component so the animated rotate output writes into this
MotionValue (for example pass the MotionValue to the motion element's
style/animate props or bind via onUpdate/useMotionValueEvent) so the rotation
value is kept in sync with the running animation and hover handlers read the
current rotation instead of the initial 0.

Comment on lines +72 to +79
<a
key={item.name}
href={item.href}
className="rounded-full p-2 text-steel transition-colors hover:text-footer-text"
>
<item.icon className="h-5 w-5" />
</a>
))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Add accessible labels to social icon links.

Social links render only icons without visible text, making them inaccessible to screen reader users. Add aria-label to convey the link purpose.

Apply this diff:

                 {social.map((item) => (
                   <a
                     key={item.name}
                     href={item.href}
+                    aria-label={item.name}
                     className="rounded-full p-2 text-steel transition-colors hover:text-footer-text"
                   >
                     <item.icon className="h-5 w-5" />
                   </a>
                 ))}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<a
key={item.name}
href={item.href}
className="rounded-full p-2 text-steel transition-colors hover:text-footer-text"
>
<item.icon className="h-5 w-5" />
</a>
))}
<a
key={item.name}
href={item.href}
aria-label={item.name}
className="rounded-full p-2 text-steel transition-colors hover:text-footer-text"
>
<item.icon className="h-5 w-5" />
</a>
))}
🤖 Prompt for AI Agents
In src/components/mikn/Footer.tsx around lines 72 to 79, the social link anchors
render only icons which are not accessible; add an aria-label to each <a> using
the item's name (or a fallback) so screen readers can announce the purpose,
e.g., set aria-label to `${item.name} social link` or simply item.name, keeping
existing props and classes unchanged.

textAlign = "center",
onLetterAnimationComplete,
}) => {
const ref = useRef<HTMLParagraphElement>(null);
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Ref type should be HTMLElement to match all possible tag types.

The ref is typed as HTMLParagraphElement, but the component can render h1h6, p, or span elements. This type mismatch could cause TypeScript issues.

-  const ref = useRef<HTMLParagraphElement>(null);
+  const ref = useRef<HTMLElement>(null);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ref = useRef<HTMLParagraphElement>(null);
const ref = useRef<HTMLElement>(null);
🤖 Prompt for AI Agents
In src/components/SplitText.tsx around line 40, the ref is declared as
useRef<HTMLParagraphElement>(null) but the component may render h1–h6, p, or
span, so change the ref generic to useRef<HTMLElement | null>(null) (or Element
| null if preferred) to match all possible tag types; update any downstream
casts/usages to either narrow to a specific element when calling
element-type-specific APIs or use only APIs available on HTMLElement to avoid
TypeScript errors.

Comment on lines +60 to +66
if (isAnimationFinished && vrm.lookAt) {
const targetX = mousePosition.x * 3;
const targetY = mousePosition.y * 3;
const targetZ = 5;
const lookAtTarget = new Vector3(targetX, targetY, targetZ);
vrm.lookAt.lookAt(lookAtTarget);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Avoid allocating Vector3 inside the render loop.

Creating a new Vector3 on every frame causes unnecessary garbage collection pressure. Cache the vector instance outside the frame loop and reuse it.

+const lookAtTarget = new Vector3();
+
 export const VRMModel: FC<{
   vrm: import("@pixiv/three-vrm").VRM | null;
   mixer: AnimationMixer | null;
   action: AnimationAction | null;
   mousePosition: { x: number; y: number };
 }> = ({ vrm, mixer, action, mousePosition }) => {
   useFrame((_state, delta) => {
     if (vrm) {
       // ...
       if (isAnimationFinished && vrm.lookAt) {
         const targetX = mousePosition.x * 3;
         const targetY = mousePosition.y * 3;
         const targetZ = 5;
-        const lookAtTarget = new Vector3(targetX, targetY, targetZ);
+        lookAtTarget.set(targetX, targetY, targetZ);
         vrm.lookAt.lookAt(lookAtTarget);
       }
       // ...
     }
   });
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (isAnimationFinished && vrm.lookAt) {
const targetX = mousePosition.x * 3;
const targetY = mousePosition.y * 3;
const targetZ = 5;
const lookAtTarget = new Vector3(targetX, targetY, targetZ);
vrm.lookAt.lookAt(lookAtTarget);
}
if (isAnimationFinished && vrm.lookAt) {
const targetX = mousePosition.x * 3;
const targetY = mousePosition.y * 3;
const targetZ = 5;
lookAtTarget.set(targetX, targetY, targetZ);
vrm.lookAt.lookAt(lookAtTarget);
}
🤖 Prompt for AI Agents
In src/components/vrm.tsx around lines 60 to 66, the render loop allocates a new
Vector3 each frame for lookAtTarget; instead, create a single cached Vector3
outside the frame loop (component or module scope) and reuse it inside the loop
by updating its x,y,z via set(x,y,z) (or setComponent) before passing it to
vrm.lookAt.lookAt(...); this removes per-frame allocations and reuses the same
instance for every frame.

Comment on lines 2 to 25
"home": {
"hi": "Hello"
},
"layout": {
"resources": "Resources",
"solutions": "Solutions",
"accountCenter": "Account Center",
"support": "Support",
"contact": "Contact Us",
"legal": "Legal",
"terms": "Terms of Service",
"privacy": "Privacy Policy",
"refunds": "Refund Policy",
"jp-payments": "特定商取引法に基づく表記"
},
"home": {
"hero": {
"blurb1": "Creators of",
"blurb2": "lots of cool stuff",
"blurb3": "making life easier.",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "Take a Look"
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Duplicate "home" key in JSON.

The "home" key appears twice (lines 2-4 and lines 17-25). JSON does not allow duplicate keys—the second occurrence will overwrite the first, causing home.hi to be lost at runtime.

Merge both into a single "home" object:

 {
   "home": {
-    "hi": "Hello"
-  },
-  "layout": {
-    "resources": "Resources",
-    ...
-  },
-  "home": {
+    "hi": "Hello",
     "hero": {
       "blurb1": "Creators of",
       "blurb2": "lots of cool stuff",
       "blurb3": "making life easier.",
       "circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
       "primaryCta": "Take a Look"
     }
-  }
+  },
+  "layout": {
+    "resources": "Resources",
+    "solutions": "Solutions",
+    "accountCenter": "Account Center",
+    "support": "Support",
+    "contact": "Contact Us",
+    "legal": "Legal",
+    "terms": "Terms of Service",
+    "privacy": "Privacy Policy",
+    "refunds": "Refund Policy",
+    "jp-payments": "特定商取引法に基づく表記"
+  }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"home": {
"hi": "Hello"
},
"layout": {
"resources": "Resources",
"solutions": "Solutions",
"accountCenter": "Account Center",
"support": "Support",
"contact": "Contact Us",
"legal": "Legal",
"terms": "Terms of Service",
"privacy": "Privacy Policy",
"refunds": "Refund Policy",
"jp-payments": "特定商取引法に基づく表記"
},
"home": {
"hero": {
"blurb1": "Creators of",
"blurb2": "lots of cool stuff",
"blurb3": "making life easier.",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "Take a Look"
}
}
{
"home": {
"hi": "Hello",
"hero": {
"blurb1": "Creators of",
"blurb2": "lots of cool stuff",
"blurb3": "making life easier.",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "Take a Look"
}
},
"layout": {
"resources": "Resources",
"solutions": "Solutions",
"accountCenter": "Account Center",
"support": "Support",
"contact": "Contact Us",
"legal": "Legal",
"terms": "Terms of Service",
"privacy": "Privacy Policy",
"refunds": "Refund Policy",
"jp-payments": "特定商取引法に基づく表記"
}
}
🤖 Prompt for AI Agents
In src/messages/en.json around lines 2 to 25 there are two "home" keys (lines
2–4 and 17–25) which collide; merge them into a single "home" object that
contains both the top-level "hi" entry and the nested "hero" object (i.e., move
"hi": "Hello" into the same "home" object that contains "hero"), remove the
duplicate "home" block, ensure the resulting JSON is valid (no duplicate keys,
proper commas/braces) and run your JSON linter or tests to confirm nothing else
was broken.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (2)
src/components/SettingsController.tsx (1)

14-33: Complete the accessibility improvements from the previous review.

The component now uses motion.button which addresses part of the previous review, but the ARIA attributes and related props are still missing.

Apply this diff to add the missing accessibility attributes:

-const AnimatedButton = ({ onClick }: { onClick: () => void }) => {
+const AnimatedButton = ({
+  onClick,
+  open,
+}: {
+  onClick: () => void;
+  open: boolean;
+}) => {
   return (
     <motion.button
+      type="button"
+      aria-label="Open site settings"
+      aria-expanded={open}
+      aria-controls="site-settings-panel"
       whileHover={{ scale: 1.2 }}
       whileTap={{
         scale: 0.8,
         borderRadius: "100%",
       }}
       onClick={onClick}
+      className="bg-primary rounded-full w-10 h-10 flex items-center justify-center"
     >
-      <div
-        className={
-          "bg-primary rounded-full w-10 h-10 flex items-center justify-center"
-        }
-      >
-        <Settings animateOnHover />
-      </div>
+      <Settings animateOnHover />
     </motion.button>
   );
 };

Also update line 43 and add id to the panel:

-      <AnimatedButton onClick={() => setOpen(!open)} />
+      <AnimatedButton open={open} onClick={() => setOpen(!open)} />
       <AnimatePresence>
         {open && (
           <motion.div
+            id="site-settings-panel"
             initial={{ opacity: 0, y: 7 }}
src/app/[locale]/template.tsx (1)

17-17: Analytics still not gated behind user consent.

The previous review flagged that useSwetrix is called unconditionally without checking user consent. This remains unaddressed and poses a privacy compliance risk.

Based on the @c15t/react documentation, wrap the analytics initialisation with consent checking:

 export default function PagesLayout({ children }: { children: ReactNode }) {
-  useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
   const t = useTranslations("layout");
+  const { hasConsented } = useConsentManager();
+  
+  useEffect(() => {
+    if (hasConsented()) {
+      useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
+    }
+  }, [hasConsented]);

Note: You'll also need to import useEffect and useConsentManager:

-import { ReactNode } from "react";
+import { ReactNode, useEffect } from "react";
+import { useConsentManager } from "@c15t/react";
🧹 Nitpick comments (4)
src/app/not-found.tsx (2)

6-10: Consider centralising font configuration in the root layout.

The font configuration is defined within the not-found page, but the AI summary indicates layout.tsx introduces a broader font-loading strategy. Duplicating font configuration across pages creates inconsistency and maintenance overhead.

Consider moving this font configuration to the root layout.tsx and importing it where needed, or reusing the font instance from the layout if it already defines Noto Sans JP.


18-18: Remove trailing space in className.

There's an unnecessary trailing space in the className string after font-bold .

Apply this diff:

-            <h1 className={"text-4xl text-center text-white font-bold "}>
+            <h1 className="text-4xl text-center text-white font-bold">
               404 - Page Not Found
             </h1>
src/components/ui/shadcn-io/spinner/index.tsx (2)

9-27: Avoid any when forwarding props to Lucide icons

You don’t need the as any casts when spreading props into the Lucide icons; using any here discards useful type‑checking for consumers of Spinner.

You can keep the same API and remove the casts:

-type SpinnerVariantProps = Omit<SpinnerProps, "variant">;
+type SpinnerVariantProps = Omit<SpinnerProps, "variant">;

 const Default = ({ className, ...props }: SpinnerVariantProps) => (
-  <LoaderIcon className={cn("animate-spin", className)} {...(props as any)} />
+  <LoaderIcon className={cn("animate-spin", className)} {...props} />
 );

 const Circle = ({ className, ...props }: SpinnerVariantProps) => (
   <LoaderCircleIcon
-    className={cn("animate-spin", className)}
-    {...(props as any)}
+    className={cn("animate-spin", className)}
+    {...props}
   />
 );

 const Pinwheel = ({ className, ...props }: SpinnerVariantProps) => (
   <LoaderPinwheelIcon
-    className={cn("animate-spin", className)}
-    {...(props as any)}
+    className={cn("animate-spin", className)}
+    {...props}
   />
 );

The same pattern can be applied to the other variants that currently use ...(props as any).


96-245: Consider motion / accessibility defaults for animated SVG variants

All non‑icon variants (Ellipsis, Ring, Bars, Infinite) are continuously animated. Depending on where these are used, that can conflict with users’ “reduced motion” preferences or screen‑reader expectations.

Two optional improvements to consider:

  • Allow callers to pass through aria-* props and/or a title/aria-label override via SpinnerProps, and document that the component is purely visual by default (e.g. aria-hidden) unless wrapped in a status container.
  • Optionally gate the CSS/SVG animation when a reducedMotion flag or a global prefers-reduced-motion hook indicates motion should be minimised.

These are non‑blocking, but would tighten a11y for a highly used primitive.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e0256fc and b881829.

📒 Files selected for processing (6)
  • src/app/[locale]/page.tsx (1 hunks)
  • src/app/[locale]/template.tsx (1 hunks)
  • src/app/not-found.tsx (1 hunks)
  • src/components/SettingsController.tsx (1 hunks)
  • src/components/ui/shadcn-io/spinner/index.tsx (1 hunks)
  • src/messages/en.json (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (4)
src/app/[locale]/page.tsx (1)
src/components/vrm.tsx (1)
  • VRM (78-204)
src/app/not-found.tsx (1)
src/components/ui/button.tsx (1)
  • Button (57-57)
src/app/[locale]/template.tsx (4)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/mikn/Header.tsx (1)
  • Header (167-433)
src/components/SettingsController.tsx (1)
  • SettingsController (35-105)
src/components/mikn/Footer.tsx (1)
  • Footer (23-87)
src/components/SettingsController.tsx (1)
src/contexts/CursorToysContext.tsx (1)
  • useCursorToys (39-44)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (6)
src/app/not-found.tsx (1)

3-3: Verify the Button component import path.

The Button is imported from @/components/animate-ui/components/buttons/button, but confirm this matches the actual component location in your codebase to avoid runtime errors.

src/app/[locale]/page.tsx (2)

1-9: Imports and client directive look correct.

The component imports are appropriate for the new client-based hero layout.


11-11: No params prop needed—this is a client component.

In Next.js App Router, only server components receive params as a prop. Since this page is a client component (using useTranslations hook), the current function signature is correct. The locale is provided via next-intl's context setup in the parent layout, and useTranslations("home") retrieves it from there.

src/messages/en.json (1)

1-26: JSON structure is now valid and resolves the duplicate key issue.

The previous critical issue with duplicate "home" keys has been addressed. The current structure correctly separates:

  • layout.home (line 13): Navigation label "Home"
  • home (line 17): Page content with nested hero object

The JSON is valid and properly structured for the page component's usage.

src/components/ui/shadcn-io/spinner/index.tsx (2)

29-48: CircleFilled variant composition looks sound

The layered LoaderCircleIcon approach with an outer low‑opacity ring and inner spinner, plus explicit size handling on the wrapper, is clear and should be easy to style via className.

No issues from my side here.


247-278: Spinner API and variant switch look coherent

The SpinnerProps union of variant values plus the centralised Spinner switch provides a clear, extensible API, and defaulting to the Default variant when variant is omitted is intuitive.

No behavioural issues spotted here.

Comment on lines 37 to 41
<Link href={"/solutions"}>
<Button variant="default" size="lg">
{t("hero.primaryCta")}
</Button>
</Link>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find src -name "button.tsx" -o -name "button.ts" -o -name "Button.tsx" | head -20

Repository: mikndotdev/mikn.dev

Length of output: 250


🏁 Script executed:

head -50 src/app/[locale]/page.tsx | grep -A5 -B5 "import.*Button"

Repository: mikndotdev/mikn.dev

Length of output: 320


🏁 Script executed:

cat src/components/animate-ui/components/buttons/button.tsx

Repository: mikndotdev/mikn.dev

Length of output: 2493


🏁 Script executed:

cat src/components/animate-ui/primitives/buttons/button.tsx

Repository: mikndotdev/mikn.dev

Length of output: 719


🏁 Script executed:

sed -n '35,45p' src/app/[locale]/page.tsx

Repository: mikndotdev/mikn.dev

Length of output: 677


Nested interactive elements: Link wrapping Button.

Wrapping a <Link> around a <Button> creates nested interactive elements (<a> > <motion.button>), which violates HTML semantics and degrades accessibility.

The Button component supports the asChild prop (via the Slot pattern) to render as a different element. Use this to render the Link directly:

-<Link href={"/solutions"}>
-  <Button variant="default" size="lg">
+<Button asChild>
+  <Link href={"/solutions"}>
     {t("hero.primaryCta")}
-  </Button>
-</Link>
+  </Link>
+</Button>

Alternatively, pass the Link as a child:

-<Link href={"/solutions"}>
-  <Button variant="default" size="lg">
+<Button variant="default" size="lg" asChild>
+  <a href="/solutions">
     {t("hero.primaryCta")}
-  </Button>
-</Link>
+  </a>
+</Button>

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/app/[locale]/page.tsx around lines 37 to 41, the code wraps a Link around
a Button which creates nested interactive elements; change this to render the
Link as the Button using the Button's asChild prop (Slot pattern) so the Button
outputs an anchor instead of nesting, i.e., make Button accept asChild and pass
Link as its child (or alternatively render Link directly and put non-interactive
content inside Button) so the resulting DOM is a single interactive anchor
element with Button styling.

Comment on lines +13 to +28
return (
<>
<html className={notoSansJP.variable} lang="en">
<body>
<div className="h-screen flex flex-col items-center justify-center">
<h1 className={"text-4xl text-center text-white font-bold "}>
404 - Page Not Found
</h1>
<Link href={"/"}>
<Button className={"mt-5"}>Take me back</Button>
</Link>
</div>
</body>
</html>
</>
);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the root layout structure
echo "=== Root layout.tsx ==="
cat src/app/layout.tsx

echo -e "\n=== Other not-found pages ==="
fd "not-found.tsx" src/app

echo -e "\n=== Current not-found.tsx content ==="
cat src/app/not-found.tsx

Repository: mikndotdev/mikn.dev

Length of output: 1617


🏁 Script executed:

# Verify Next.js not-found.tsx behavior and check if globals.css is imported elsewhere
echo "=== Checking globals.css imports ==="
rg "globals.css" src/app --type tsx --type ts

echo -e "\n=== Checking all layout files ==="
fd "layout.tsx" src/app

Repository: mikndotdev/mikn.dev

Length of output: 226


🏁 Script executed:

# Check the [locale] layout to understand the multi-layout structure
cat src/app/\[locale\]/layout.tsx

Repository: mikndotdev/mikn.dev

Length of output: 1340


Remove html and body tags from the not-found page—they are duplicated in the root layout.

In Next.js App Router, src/app/layout.tsx defines the top-level html and body elements. The not-found.tsx page should return only its content, not recreate these structural elements. Currently, returning html and body from the not-found component creates nested/duplicate elements, which is invalid HTML and violates App Router conventions.

Additionally, the React.Fragment wrapper (<></>) is unnecessary since there's only one child element.

Keep the globals.css import as it's needed for the not-found page styling (since it bypasses the [locale] layout where globals.css is imported), but refactor to return only the content:

 export default function NotFound() {
   return (
-    <>
-      <html className={notoSansJP.variable} lang="en">
-        <body>
-          <div className="h-screen flex flex-col items-center justify-center">
-            <h1 className={"text-4xl text-center text-white font-bold "}>
-              404 - Page Not Found
-            </h1>
-            <Link href={"/"}>
-              <Button className={"mt-5"}>Take me back</Button>
-            </Link>
-          </div>
-        </body>
-      </html>
-    </>
+    <div className="h-screen flex flex-col items-center justify-center">
+      <h1 className="text-4xl text-center text-white font-bold">
+        404 - Page Not Found
+      </h1>
+      <Link href="/">
+        <Button className="mt-5">Take me back</Button>
+      </Link>
+    </div>
   );
 }

Also remove the duplicate notoSansJP font initialization from this file, as it's already applied in the root layout.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return (
<>
<html className={notoSansJP.variable} lang="en">
<body>
<div className="h-screen flex flex-col items-center justify-center">
<h1 className={"text-4xl text-center text-white font-bold "}>
404 - Page Not Found
</h1>
<Link href={"/"}>
<Button className={"mt-5"}>Take me back</Button>
</Link>
</div>
</body>
</html>
</>
);
export default function NotFound() {
return (
<div className="h-screen flex flex-col items-center justify-center">
<h1 className="text-4xl text-center text-white font-bold">
404 - Page Not Found
</h1>
<Link href="/">
<Button className="mt-5">Take me back</Button>
</Link>
</div>
);
}
🤖 Prompt for AI Agents
In src/app/not-found.tsx around lines 13 to 28, remove the duplicate top-level
html and body elements and the unnecessary React fragment: the component should
only return the inner content (the centered div with heading, Link and Button).
Keep the globals.css import but delete the duplicate notoSansJP font
initialization and any use of its className here (the root layout already
applies the font). Ensure the component exports a simple React component that
returns only the content markup (no html/body wrappers, no fragment).

Comment on lines +90 to +92
<a
href={`https://github.com/mikndotdev/mikn.dev/blob/main/src/app/%5Blocale%5D${pathname}page.tsx`}
target="_blank"
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fragile URL construction for page source link.

The GitHub URL is constructed by hardcoding the repository structure and URL-encoding [locale]. This will break if:

  • The repository is renamed or moved
  • The branch changes from main
  • The file is not named page.tsx
  • The path structure changes

Consider moving this to a configuration file or environment variable, or using a more robust URL construction method that handles edge cases.

🤖 Prompt for AI Agents
In src/components/SettingsController.tsx around lines 90 to 92, the GitHub link
is being built with a hardcoded repo path, branch, and encoded literal
“[locale]”, which is fragile; change it to read a configurable base repository
URL and branch (from environment variables or a central config) and construct
the file path programmatically using the current locale and pathname, encoding
path segments with encodeURIComponent and joining them safely so renames/branch
changes/path variations are handled, and add a fallback or unit test for missing
config values.

Comment on lines +50 to +94
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
return (
<svg
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...(props as any)}
>
<title>Loading...</title>
<circle cx="4" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0;ellipsis3.end+0.25s"
calcMode="spline"
dur="0.6s"
id="ellipsis1"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="12" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.1s"
calcMode="spline"
dur="0.6s"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="20" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.2s"
calcMode="spline"
dur="0.6s"
id="ellipsis3"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
</svg>
);
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

find . -name "spinner" -o -name "index.tsx" | head -20

Repository: mikndotdev/mikn.dev

Length of output: 148


🏁 Script executed:

fd spinner index.tsx

Repository: mikndotdev/mikn.dev

Length of output: 159


🏁 Script executed:

fd -t f "index.tsx" | grep -i spinner

Repository: mikndotdev/mikn.dev

Length of output: 108


🏁 Script executed:

rg -l "Ellipsis" --type tsx

Repository: mikndotdev/mikn.dev

Length of output: 90


🏁 Script executed:

cat -n src/components/ui/shadcn-io/spinner/index.tsx | sed -n '40,100p'

Repository: mikndotdev/mikn.dev

Length of output: 2009


Remove global SMIL animation IDs to prevent conflicts across multiple instances

The Ellipsis component uses global IDs (ellipsis1, ellipsis3) to chain animations via begin="ellipsis3.end+0.25s" and begin="ellipsis1.begin+0.1s". When multiple <Ellipsis /> instances render on the same page, these duplicate IDs create ambiguous animation timing and cause unpredictable behavior.

Replace the cross-instance ID references with self-contained animations using absolute timing offsets and repeatCount="indefinite":

-      <circle cx="4" cy="12" fill="currentColor" r="2">
-        <animate
-          attributeName="cy"
-          begin="0;ellipsis3.end+0.25s"
-          calcMode="spline"
-          dur="0.6s"
-          id="ellipsis1"
-          keySplines=".33,.66,.66,1;.33,0,.66,.33"
-          values="12;6;12"
-        />
-      </circle>
+      <circle cx="4" cy="12" fill="currentColor" r="2">
+        <animate
+          attributeName="cy"
+          begin="0s"
+          calcMode="spline"
+          dur="0.6s"
+          repeatCount="indefinite"
+          keySplines=".33,.66,.66,1;.33,0,.66,.33"
+          values="12;6;12"
+        />
+      </circle>
       <circle cx="12" cy="12" fill="currentColor" r="2">
         <animate
           attributeName="cy"
-          begin="ellipsis1.begin+0.1s"
+          begin="0.1s"
           calcMode="spline"
           dur="0.6s"
+          repeatCount="indefinite"
           keySplines=".33,.66,.66,1;.33,0,.66,.33"
           values="12;6;12"
         />
       </circle>
       <circle cx="20" cy="12" fill="currentColor" r="2">
         <animate
           attributeName="cy"
-          begin="ellipsis1.begin+0.2s"
+          begin="0.2s"
           calcMode="spline"
           dur="0.6s"
-          id="ellipsis3"
+          repeatCount="indefinite"
           keySplines=".33,.66,.66,1;.33,0,.66,.33"
           values="12;6;12"
         />
       </circle>

Each circle animates independently with staggered offsets (0s, 0.1s, 0.2s) while maintaining the same visual effect.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
return (
<svg
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...(props as any)}
>
<title>Loading...</title>
<circle cx="4" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0;ellipsis3.end+0.25s"
calcMode="spline"
dur="0.6s"
id="ellipsis1"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="12" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.1s"
calcMode="spline"
dur="0.6s"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="20" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="ellipsis1.begin+0.2s"
calcMode="spline"
dur="0.6s"
id="ellipsis3"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
</svg>
);
};
const Ellipsis = ({ size = 24, ...props }: SpinnerVariantProps) => {
return (
<svg
height={size}
viewBox="0 0 24 24"
width={size}
xmlns="http://www.w3.org/2000/svg"
{...(props as any)}
>
<title>Loading...</title>
<circle cx="4" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0s"
calcMode="spline"
dur="0.6s"
repeatCount="indefinite"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="12" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0.1s"
calcMode="spline"
dur="0.6s"
repeatCount="indefinite"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
<circle cx="20" cy="12" fill="currentColor" r="2">
<animate
attributeName="cy"
begin="0.2s"
calcMode="spline"
dur="0.6s"
repeatCount="indefinite"
keySplines=".33,.66,.66,1;.33,0,.66,.33"
values="12;6;12"
/>
</circle>
</svg>
);
};
🤖 Prompt for AI Agents
In src/components/ui/shadcn-io/spinner/index.tsx around lines 50-94, remove the
global SMIL animation IDs and cross-reference strings (ellipsis1, ellipsis3) and
instead make each circle animate independently by deleting id attributes and
replacing the begin attributes with fixed staggered absolute offsets (e.g.,
"0s", "0.1s", "0.2s") and add repeatCount="indefinite" to each <animate>; keep
existing dur, values, calcMode and keySplines so the timing/easing stays the
same but no global IDs or cross-instance references remain.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
package.json (1)

55-55: Update React DOM types to match React DOM 19.

@types/react-dom is pinned to ^18, but react-dom is at ^19.2.1 (line 40). This version mismatch will cause type errors with React 19 DOM APIs.

Update to align with React 19:

-    "@types/react-dom": "^18",
+    "@types/react-dom": "^19",

Then regenerate your lockfile and verify TypeScript compatibility.

🧹 Nitpick comments (2)
src/components/ScrollFloat.tsx (2)

7-9: Consider narrowing children type for clarity.

The component only handles string children (line 33), but the type allows any ReactNode. Non-string children will silently render as empty text.

 interface ScrollFloatProps {
-  children: ReactNode;
+  children: string;
   scrollContainerRef?: RefObject<HTMLElement>;

78-82: Consider making the wrapper element configurable.

The component always renders an <h2>, which may not be semantically appropriate in all contexts (e.g., when used for non-heading decorative text).

You could add an optional as prop to allow consumers to specify the element type:

as?: keyof JSX.IntrinsicElements;
// ...
const Element = as || 'h2';
return <Element ref={containerRef} ... />
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b881829 and 4dba606.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • package.json (1 hunks)
  • src/app/api/[...slugs]/route.ts (1 hunks)
  • src/components/ScrollFloat.tsx (1 hunks)
  • src/messages/en.json (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Cloudflare Pages
🔇 Additional comments (7)
src/messages/en.json (1)

1-26: JSON structure resolved and valid.

The previous critical issue regarding duplicate "home" keys has been fixed. The file now contains a single "layout" object (lines 2–16) with navigation and layout-related strings, and a single "home" object (lines 17–25) containing the hero section content. The JSON is syntactically valid with no duplicate keys or collisions.

src/components/ScrollFloat.tsx (3)

1-5: LGTM!

Imports and module-level GSAP plugin registration are correctly structured.


32-39: LGTM!

Character splitting logic is well-memoised. Using index as key is acceptable here since the character array is static and never reordered.


85-85: LGTM!

Default export is appropriate for this single-component module.

src/app/api/[...slugs]/route.ts (3)

3-3: LGTM!

The Elysia instance initialisation with the /api prefix aligns correctly with the Next.js API route structure.


5-5: LGTM!

The hello route definition is clean and appropriate for a basic GET endpoint.


9-9: LGTM!

The type export enables type-safe API client integration, which is good practice.

Comment on lines 7 to 8
export const GET = app.fetch
export const POST = app.fetch
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove POST export or define POST routes.

The POST handler is exported but no POST routes are defined in the application. This creates a misleading API surface where POST requests will receive 404 responses despite the export suggesting POST is supported.

Consider one of these solutions:

Solution 1 (preferred): Remove the POST export if not needed.

 export const GET = app.fetch
-export const POST = app.fetch
 export type API = typeof app

Solution 2: Add a POST route if POST functionality is required.

 app.get('/hello', 'Hello Nextjs')
+app.post('/hello', () => ({ message: 'Hello from POST' }))

 export const GET = app.fetch
 export const POST = app.fetch
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const GET = app.fetch
export const POST = app.fetch
export const GET = app.fetch
export type API = typeof app
🤖 Prompt for AI Agents
In src/app/api/[...slugs]/route.ts around lines 7-8, the file exports POST =
app.fetch but no POST routes are implemented which misrepresents the API
surface; either remove the POST export if POST is not supported (preferred) or
implement actual POST handling by adding POST routes and wiring them into
app.fetch handling so POST requests are routed and return appropriate responses;
update exports accordingly and run tests to confirm no 404s for intended
endpoints.

Comment on lines 41 to 76
useEffect(() => {
const el = containerRef.current;
if (!el) return;

const scroller = scrollContainerRef && scrollContainerRef.current ? scrollContainerRef.current : window;

const charElements = el.querySelectorAll('.inline-block');

gsap.fromTo(
charElements,
{
willChange: 'opacity, transform',
opacity: 0,
yPercent: 120,
scaleY: 2.3,
scaleX: 0.7,
transformOrigin: '50% 0%'
},
{
duration: animationDuration,
ease: ease,
opacity: 1,
yPercent: 0,
scaleY: 1,
scaleX: 1,
stagger: stagger,
scrollTrigger: {
trigger: el,
scroller,
start: scrollStart,
end: scrollEnd,
scrub: true
}
}
);
}, [scrollContainerRef, animationDuration, ease, scrollStart, scrollEnd, stagger]);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing cleanup causes memory leak and stale ScrollTrigger instances.

The useEffect creates a GSAP animation with ScrollTrigger but never cleans it up on unmount. This will leak memory and leave orphaned triggers. Additionally, children should be in the dependency array to re-animate when text changes.

Apply this diff to add proper cleanup:

   useEffect(() => {
     const el = containerRef.current;
     if (!el) return;

     const scroller = scrollContainerRef && scrollContainerRef.current ? scrollContainerRef.current : window;

     const charElements = el.querySelectorAll('.inline-block');

-    gsap.fromTo(
+    const animation = gsap.fromTo(
       charElements,
       {
         willChange: 'opacity, transform',
         opacity: 0,
         yPercent: 120,
         scaleY: 2.3,
         scaleX: 0.7,
         transformOrigin: '50% 0%'
       },
       {
         duration: animationDuration,
         ease: ease,
         opacity: 1,
         yPercent: 0,
         scaleY: 1,
         scaleX: 1,
         stagger: stagger,
         scrollTrigger: {
           trigger: el,
           scroller,
           start: scrollStart,
           end: scrollEnd,
           scrub: true
         }
       }
     );
+
+    return () => {
+      animation.scrollTrigger?.kill();
+      animation.kill();
+    };
-  }, [scrollContainerRef, animationDuration, ease, scrollStart, scrollEnd, stagger]);
+  }, [scrollContainerRef, animationDuration, ease, scrollStart, scrollEnd, stagger, children]);
🤖 Prompt for AI Agents
In src/components/ScrollFloat.tsx around lines 41 to 76, the effect creates a
GSAP animation with ScrollTrigger but never cleans it up and also omits children
from the dependency array; modify the effect to capture the GSAP context/tween
(or created animation instance) and return a cleanup function that kills the
tween and associated ScrollTrigger(s) on unmount or re-run (e.g., call
tween.kill() and ScrollTrigger.kill() or use gsap.context() and ctx.revert()),
and add children to the dependency array so the animation re-initializes when
text/children change.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 8

♻️ Duplicate comments (5)
package.json (1)

56-56: Update React DOM types to match React DOM 19.

The project uses react-dom 19.2.1 (line 41) but @types/react-dom ^18. This mismatch can cause type errors with React 19 DOM APIs.

Apply this diff:

-    "@types/react-dom": "^18",
+    "@types/react-dom": "^19",

Then regenerate your lockfile and run the typechecker to verify compatibility.

src/app/[locale]/template.tsx (1)

17-17: Gate analytics behind user consent

useSwetrix is called unconditionally. To meet privacy expectations, initialise analytics only after the user consents via ConsentManager.

src/app/[locale]/(pages)/template.tsx (1)

17-17: Gate analytics behind user consent

Same issue as in src/app/[locale]/template.tsx - useSwetrix is called unconditionally without checking consent state.

src/components/fancy/blocks/simple-marquee.tsx (1)

113-122: Fix wrap range: arguments are inverted (min/max swapped).

This issue was previously flagged. Passing min=0 and max=-100 produces a negative range, causing incorrect wrapping behaviour. The arguments should be swapped so the range is positive.

🔎 Proposed fix
   const x = useTransform(baseX, (v) => {
-    const wrappedValue = wrap(0, -100, v);
+    const wrappedValue = wrap(-100, 0, v);
     return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`;
   });
   const y = useTransform(baseY, (v) => {
-    const wrappedValue = wrap(0, -100, v);
+    const wrappedValue = wrap(-100, 0, v);
     return `${easing ? easing(wrappedValue / -100) * -100 : wrappedValue}%`;
   });
src/app/[locale]/page.tsx (1)

48-52: Nested interactive elements: Link wrapping Button.

This issue was previously flagged. Wrapping a <Link> around a <Button> creates nested interactive elements (<a> > <button>), which violates HTML semantics and degrades accessibility. Use the Button's asChild prop to compose these correctly.

🔎 Proposed fix using asChild pattern
-            <Link href={"/solutions"}>
-              <Button variant="default" size="lg">
-                {t("hero.primaryCta")}
-              </Button>
-            </Link>
+            <Button variant="default" size="lg" asChild>
+              <Link href={"/solutions"}>
+                {t("hero.primaryCta")}
+              </Link>
+            </Button>
🧹 Nitpick comments (14)
src/components/ui/card.tsx (2)

32-42: Consider using a heading element for CardTitle.

CardTitle currently renders as a div, but card titles should semantically be heading elements (h1-h6) for proper document structure and accessibility. Screen readers rely on heading hierarchy for navigation, and this impacts SEO as well.

🔎 Recommended refactor to support semantic headings

One approach is to accept an as prop with a sensible default:

+type CardTitleProps = React.HTMLAttributes<HTMLHeadingElement> & {
+  as?: "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
+};
+
 const CardTitle = React.forwardRef<
-  HTMLDivElement,
-  React.HTMLAttributes<HTMLDivElement>
->(({ className, ...props }, ref) => (
-  <div
+  HTMLHeadingElement,
+  CardTitleProps
+>(({ className, as: Component = "h3", ...props }, ref) => (
+  <Component
     ref={ref}
     className={cn("font-semibold leading-none tracking-tight", className)}
     {...props}
   />
 ));

This allows consumers to specify the appropriate heading level whilst defaulting to h3.


44-54: Consider using a paragraph element for CardDescription.

Whilst the current implementation works, descriptions are semantically text paragraphs and would be better represented with a <p> element rather than a <div> for improved semantic HTML structure.

🔎 Optional refactor
 const CardDescription = React.forwardRef<
-  HTMLDivElement,
-  React.HTMLAttributes<HTMLDivElement>
+  HTMLParagraphElement,
+  React.HTMLAttributes<HTMLParagraphElement>
 >(({ className, ...props }, ref) => (
-  <div
+  <p
     ref={ref}
     className={cn("text-sm text-muted-foreground", className)}
     {...props}
   />
 ));
AGENTS.md (2)

12-12: Clarify package manager guidance.

Line 12 mentions both "npm" and "bun.lock", which may confuse agents about the intended package manager. Given the repeated emphasis on using Bun throughout the guide (lines 17–20), consider clarifying whether npm is the declared package manager in package.json but Bun is the recommended runtime, or whether Bun is the primary package manager.


43-43: Fix grammar: use "to prefer" construction.

The sentence "Prefer interfaces over types for object definitions" reads awkwardly. Restructure to "Prefer to use interfaces over types for object definitions" or "Use interfaces over types for object definitions" for clarity.

🔎 Proposed grammar fix
-- **Types:** Prefer interfaces over types for object definitions. Explicitly type component props.
+- **Types:** Use interfaces over types for object definitions. Explicitly type component props.
src/components/FaultyTerminal.tsx (3)

235-248: Consider adding input validation for malformed hex strings.

The hexToRgb function doesn't validate the input, so malformed hex strings (e.g., "gggggg" or "abc" that isn't valid shorthand) would produce NaN values, causing unexpected shader behaviour. Since this is an internal utility with a typed prop, the risk is low, but a simple validation could improve robustness.

🔎 Optional fix with validation
 function hexToRgb(hex: string): [number, number, number] {
   let h = hex.replace("#", "").trim();
   if (h.length === 3)
     h = h
       .split("")
       .map((c) => c + c)
       .join("");
+  if (!/^[0-9a-fA-F]{6}$/.test(h)) {
+    return [1, 1, 1]; // fallback to white
+  }
   const num = parseInt(h, 16);
   return [
     ((num >> 16) & 255) / 255,
     ((num >> 8) & 255) / 255,
     (num & 255) / 255,
   ];
 }

415-435: Potential unnecessary effect re-runs due to array reference in dependencies.

gridMul is an array prop included in the dependency array. If the parent component passes an inline array like gridMul={[2, 1]}, a new array reference is created on every render, causing this effect to re-run and recreate the entire WebGL context unnecessarily. This could cause performance degradation and visual flicker.

🔎 Suggested approaches

Option 1: Stringify or spread the array in the dependency array:

 }, [
   dpr,
   pause,
   timeScale,
   scale,
-  gridMul,
+  gridMul[0],
+  gridMul[1],
   digitSize,
   // ...
 ]);

Option 2: Document that consumers should memoize the gridMul prop, or memoize it internally:

const stableGridMul = useMemo(() => gridMul, [gridMul[0], gridMul[1]]);

Then use stableGridMul in the effect and dependency array.


437-444: Class name concatenation may include "undefined" string.

When className is not provided, the template literal will produce "w-full h-full relative overflow-hidden undefined". Consider using a utility like cn (mentioned in the PR's utils) or a fallback.

🔎 Proposed fix
+import { cn } from "@/lib/utils";
+
 // ...
 
   return (
     <div
       ref={containerRef}
-      className={`w-full h-full relative overflow-hidden ${className}`}
+      className={cn("w-full h-full relative overflow-hidden", className)}
       style={style}
       {...rest}
     />
   );

Or without an external utility:

-      className={`w-full h-full relative overflow-hidden ${className}`}
+      className={`w-full h-full relative overflow-hidden ${className ?? ""}`}
src/app/[locale]/(pages)/contact/page.tsx (1)

76-80: Consider adding target="_blank" for external Discord link

External links typically benefit from opening in a new tab to preserve user context on the current page.

🔎 Proposed fix
-              <Link href="https://discord.gg/2892hjFQn8">
+              <Link href="https://discord.gg/2892hjFQn8" target="_blank" rel="noopener noreferrer">
                 <Button variant="default" size="lg">
                   {t("join")}
                 </Button>
               </Link>
src/app/[locale]/template.tsx (1)

96-96: Empty className attribute

The empty className={""} serves no purpose. Consider removing it or adding the intended styling (the (pages)/template.tsx uses "py-24 px-4 md:px-30").

🔎 Proposed fix
-        <div className={""}>{children}</div>
+        <div>{children}</div>
src/app/[locale]/(pages)/template.tsx (1)

16-117: Consider extracting shared layout logic

This component is nearly identical to src/app/[locale]/template.tsx. The social, links, and navigation arrays, along with the layout composition, are duplicated. Consider extracting the shared configuration and layout into a reusable component or hook.

src/components/fancy/blocks/simple-marquee.tsx (1)

132-139: Inconsistent decay factor: hardcoded 0.9 vs configurable dragVelocityDecay.

During active dragging, velocity decays using a hardcoded 0.9 (line 134), whilst the configurable dragVelocityDecay prop (default 0.96) is only used when not dragging (line 181). This inconsistency may confuse users expecting the prop to control all decay behaviour.

Consider using the prop value consistently, or document that the prop only affects post-drag decay.

🔎 Proposed fix for consistency
       // Add decay to dragVelocity when not moving
       // This will gradually reduce the velocity to zero when the pointer isn't moving
-      dragVelocity.current *= 0.9;
+      dragVelocity.current *= dragVelocityDecay;
src/app/[locale]/page.tsx (3)

27-28: Empty placeholder div.

This div with absolute inset-0 has no content. If it's a placeholder for a future background effect, consider adding a comment. Otherwise, it can be removed.


91-98: Consider passing static import directly to Image src.

Using NeodyLogo.src extracts just the URL string, losing the static import metadata (width, height, blurDataURL) that Next.js Image uses for optimisation. Consider passing the import directly:

-              src={NeodyLogo.src}
+              src={NeodyLogo}

This applies to all Image components in this section (RTLogo, KuronekoLogo, TakasumiLogo).


91-91: Add rel="noopener noreferrer" to external links.

External links with target="_blank" should include rel="noopener noreferrer" for security (prevents reverse tabnabbing) and privacy. Whilst modern browsers handle noopener automatically, explicit declaration is still best practice.

🔎 Example fix
-            <Link href={"https://neody.land"} target="_blank">
+            <Link href={"https://neody.land"} target="_blank" rel="noopener noreferrer">

Apply similarly to all external links.

Also applies to: 100-100, 109-109, 118-118

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4dba606 and 26eeb87.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (15)
  • AGENTS.md
  • opencode.json
  • package.json
  • src/app/[locale]/(pages)/contact/page.tsx
  • src/app/[locale]/(pages)/template.tsx
  • src/app/[locale]/page.tsx
  • src/app/[locale]/template.tsx
  • src/app/api/[...slugs]/route.ts
  • src/components/FaultyTerminal.tsx
  • src/components/ScrollFloat.tsx
  • src/components/consent-manager.tsx
  • src/components/fancy/blocks/simple-marquee.tsx
  • src/components/ui/card.tsx
  • src/messages/en.json
  • src/messages/ja.json
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/app/api/[...slugs]/route.ts
  • src/components/consent-manager.tsx
  • src/components/ScrollFloat.tsx
🧰 Additional context used
🧬 Code graph analysis (5)
src/app/[locale]/(pages)/contact/page.tsx (1)
src/components/ui/card.tsx (2)
  • Card (77-77)
  • CardContent (82-82)
src/app/[locale]/template.tsx (5)
src/app/[locale]/(pages)/template.tsx (1)
  • PagesLayout (16-117)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/mikn/Header.tsx (1)
  • Header (167-433)
src/components/SettingsController.tsx (1)
  • SettingsController (35-105)
src/components/mikn/Footer.tsx (1)
  • Footer (23-87)
src/components/fancy/blocks/simple-marquee.tsx (1)
src/lib/src/lib/utils.ts (1)
  • cn (4-6)
src/app/[locale]/(pages)/template.tsx (4)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/mikn/Header.tsx (1)
  • Header (167-433)
src/components/SettingsController.tsx (1)
  • SettingsController (35-105)
src/components/mikn/Footer.tsx (1)
  • Footer (23-87)
src/app/[locale]/page.tsx (1)
src/components/vrm.tsx (1)
  • VRM (78-204)
🪛 LanguageTool
AGENTS.md

[grammar] ~43-~43: Consider using “to” with “prefer”.
Context: ...ic modules to be stricter. - Types: Prefer interfaces over types for object definitions. Explicitly type...

(PREFER_OVER_TO)

🔇 Additional comments (22)
src/components/ui/card.tsx (6)

1-3: LGTM!

The imports are clean and correctly structured. The cn utility integration follows the expected pattern for Tailwind class composition.


5-18: Well-implemented Card component.

The Card component correctly uses React.forwardRef with proper TypeScript typing, spreads props appropriately, and applies sensible default Tailwind styles. The displayName is set for better debugging.


20-30: LGTM!

CardHeader correctly implements the forwarding ref pattern with appropriate flex layout styles.


56-62: LGTM!

CardContent is cleanly implemented with appropriate padding styles.


64-74: LGTM!

CardFooter correctly uses flex layout and follows the same forwarding ref pattern as other components.


76-83: LGTM!

All components are properly exported for consumption.

AGENTS.md (1)

1-75: Comprehensive guidance document is well-structured.

Overall, the AGENTS.md guide is thorough and provides clear operational standards for the team. The sections are logically organised, the tone is appropriately directive for agent behaviour, and the specific guidance on imports, component naming, styling libraries, and i18n is practical. Once the Next.js version is verified and the minor clarifications are addressed, this will serve as an excellent reference artefact.

src/components/FaultyTerminal.tsx (4)

1-27: LGTM!

The imports and props interface are well-structured. The FaultyTerminalProps interface properly extends React.HTMLAttributes<HTMLDivElement> allowing standard div props to be passed through.


29-233: LGTM!

The GLSL shader implementation is well-structured with clear separation of concerns between noise generation, pattern functions, and visual effects. The conditional checks for optional features (curvature, chromatic aberration, dither) help with performance by skipping unnecessary calculations.


273-288: LGTM!

The refs are appropriately initialised for tracking WebGL objects and animation state. The memoised tintVec and ditherValue correctly handle their transformations.


290-297: LGTM!

The mouse handler correctly calculates normalised coordinates relative to the container, with appropriate Y-axis inversion for WebGL's coordinate system.

package.json (2)

5-11: LGTM! Scripts section cleaned up appropriately.

The scripts have been streamlined to core frontend development commands, with orphaned Cloudflare-specific scripts successfully removed as per previous review feedback.


34-34: No issues identified. Next.js 16.0.8 is a stable release (released 3 December 2025) and is production-ready.

src/app/[locale]/(pages)/contact/page.tsx (1)

18-89: LGTM - Clean contact page structure

The component is well-organised with proper responsive design, good use of translations for labels, and clear visual hierarchy using Cards.

src/app/[locale]/template.tsx (1)

83-116: LGTM - Layout composition

The component properly composes the CursorToys wrapper around Header, content, SettingsController, and Footer. Translation usage and prop passing are correct.

src/app/[locale]/(pages)/template.tsx (1)

83-116: LGTM - Layout structure is correct

The component composition with CursorToys, Header, content area with responsive padding, SettingsController, and Footer is well-structured.

src/messages/en.json (2)

2-16: LGTM - Layout translations are well-structured

The layout object contains all necessary navigation and footer link translations. The duplicate key issue from the previous review has been resolved.


17-39: LGTM - Translation content

The home and contact sections are well-organised with appropriate keys matching the component usage.

src/components/fancy/blocks/simple-marquee.tsx (3)

1-20: LGTM on imports and utility function.

The imports from motion/react are appropriate for the animation requirements, and the custom wrap function correctly implements modular arithmetic for continuous looping.


22-42: Well-structured props interface.

The interface is comprehensive with good inline documentation. The prop types are appropriate for the component's functionality.


259-272: Good accessibility handling with aria-hidden.

Correctly hiding duplicate clones from screen readers with aria-hidden={i > 0} prevents assistive technology from reading repeated content.

src/app/[locale]/page.tsx (1)

1-1: Client component conversion may affect SEO.

Converting this page to a client component ("use client") means the content won't be server-rendered for search engine crawlers by default. For a landing/index page, this could impact SEO. Consider whether critical content (hero text, headings) should be statically rendered.

If SEO is important for this page, you may want to keep the page as a server component and only wrap interactive sections in client components.

Also applies to: 20-22

Comment on lines +1 to +20
{
"mcp": {
"dev3000": {
"type": "remote",
"url": "http://localhost:3684/mcp",
"enabled": true
},
"context7": {
"type": "remote",
"url": "https://mcp.context7.com/mcp",
"enabled": true
},
"shadcn": {
"type": "local",
"command": ["bunx --bun", "shadcn@latest", "mcp"],
"enabled": true
}
},
"$schema": "https://opencode.ai/config.json"
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

OpenCode AI MCP configuration schema authentication opencode.json

💡 Result:

OpenCode’s opencode.json MCP configuration supports two MCP types (local and remote) and optional authentication. Key fields and authentication options:

  • Top-level: mcp: { "": { ... } } — each MCP has a unique name. [1][2]
  • Common fields:
    • type: "local" | "remote". [2]
    • url: string (for remote MCP). [1][2]
    • command: array (for local MCP). [3]
    • headers: object — custom request headers (can include env variables like "{env:VAR}"). [2]
    • timeout: number (ms) — timeout when fetching tools. [1]
    • enabled: boolean, environment: object (for local). [3]
  • OAuth / authentication:
    • OpenCode will automatically handle OAuth for remote MCPs: it detects 401, initiates OAuth flow, uses Dynamic Client Registration (RFC 7591) if available, and securely stores tokens. No special config is required for typical OAuth servers (you can still provide headers or env-based API keys). Example minimal remote MCP with OAuth handled automatically:
      {
      "$schema":"https://opencode.ai/config.json",
      "mcp": {
      "my-oauth-server": {
      "type":"remote",
      "url":"https://mcp.example.com/mcp"
      }
      }
      }
      . [1]
    • If a server requires API-key style auth, use headers with environment substitution: "headers": { "X-API-KEY": "{env:API_KEY}" }. [2]
  • Notes:
    • OAuth support is described in the MCP docs and OpenCode will prompt to authenticate when first using an auth‑required remote MCP. [1][2]

Sources:
[1] OpenCode MCP servers (OAuth section & examples).
[2] OpenCode Config / MCP examples (headers, schema, timeout).
[3] OpenCode docs (local MCP command & environment examples).


🌐 Web query:

OpenCode MCP remote endpoints authentication credentials requirements

💡 Result:

Summary — OpenCode MCP remote endpoint auth requirements

  • Remote MCP must be declared with "type":"remote" and a reachable URL. You may send custom request headers (e.g., Authorization). [1]
  • API key / bearer token: common pattern is to put Authorization: Bearer in the mcp.headers or use an environment variable (examples show ND_MCP_BEARER_TOKEN or replacing header with ${ENV_VAR}). [2]
  • OAuth: OpenCode will attempt to handle OAuth automatically — it can detect 401, run OAuth flow, and use Dynamic Client Registration (RFC 7591) if available. You can also supply explicit oauth settings (clientId, clientSecret, scope) or disable auto-detection. Use opencode mcp auth / mcp list / mcp logout to manage credentials. Tokens are stored in ~/.local/share/opencode/mcp-auth.json. [1]
  • Workarounds: if a server or workflow doesn’t support OpenCode’s built-in flows yet, common workarounds are using mcp-remote or a local bridge and performing a one‑time manual login to pick up credentials. [3]

If you want, tell me the specific MCP endpoint you’re connecting to and I’ll give exact example opencode.json and env var/header settings.

Sources: OpenCode docs (MCP servers). [1] Netdata + OpenCode MCP examples (bearer token env var). [2] Community guide showing mcp-remote workaround and auth note. [3]


🌐 Web query:

context7.com MCP authentication dev3000 localhost configuration

💡 Result:

Short how-to: configure Context7 MCP for dev3000/localhost with authentication

  1. Choose remote or local MCP:

    • Remote (HTTP): point your MCP URL to https://mcp.context7.com/mcp and send an Authorization header. Example mcp.json entry:
      {
      "mcpServers": {
      "context7": {
      "type": "http",
      "url": "https://mcp.context7.com/mcp",
      "headers": { "Authorization": "Bearer YOUR_API_KEY" }
      }
      }
      }
    • Local (stdio): run the Context7 MCP shim locally via npx and reference it as a stdio/local command:
      {
      "mcpServers": {
      "context7": {
      "type": "stdio",
      "command": "npx",
      "args": ["-y","@upstash/context7-mcp","--api-key","YOUR_API_KEY"]
      }
      }
      }
      (Docs show both remote and local examples). [1][2]
  2. Obtain an API key from the Context7 dashboard (keys look like ctx7sk-...) and store it securely; use that key as the Bearer token or pass it via the --api-key arg or CONTEXT7_API_KEY env var. [2]

  3. dev3000 / localhost specifics:

    • If your dev tool (dev3000) supports adding MCP servers, set the server URL to the remote endpoint above or configure it to launch the npx local shim (examples in the installation docs). Ensure dev3000 is configured to send the Authorization header or to run the npx command with --api-key. [1][2][3]
  4. Test: call the MCP endpoint from your tool and verify you get successful responses (200/stream) rather than 401. If you see 429, check rate limits. [1]

Sources: Context7 API/installation docs and MCP repo examples. [1] https://context7.com/docs/api-guide [2] https://context7.com/docs/installation [3] https://github.com/mcp/upstash/context7


Configure authentication headers for Context7 and verify dev3000 requirements.

The Context7 MCP endpoint requires API key authentication, but the configuration lacks the required headers field. Add authentication using environment variables:

"context7": {
  "type": "remote",
  "url": "https://mcp.context7.com/mcp",
  "headers": { "Authorization": "Bearer {env:CONTEXT7_API_KEY}" },
  "enabled": true
}

Store the API key (format: ctx7sk-...) in an environment variable rather than committing it to the repository. Verify whether the dev3000 endpoint on localhost:3684 also requires authentication and configure headers if needed. Note that OpenCode can handle OAuth automatically by detecting 401 responses, but explicit API keys require manual header configuration.

🤖 Prompt for AI Agents
In opencode.json around lines 1 to 20, Context7's remote MCP entry is missing
the required Authorization header; add a headers object that sets
"Authorization" to "Bearer {env:CONTEXT7_API_KEY}" (using an environment
variable for the ctx7sk-... key) and ensure the value is not committed to source
control; also check the dev3000 entry on localhost:3684 to see if it requires
authentication and, if so, add a similar headers field referencing an
appropriate env var for dev3000.

Comment on lines +3 to +7
"dev3000": {
"type": "remote",
"url": "http://localhost:3684/mcp",
"enabled": true
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Remove or conditionalise the localhost MCP endpoint.

The dev3000 MCP is configured to connect to http://localhost:3684/mcp, which will only function in local development. If this configuration file is deployed to production or staging environments, the application will attempt to connect to localhost on the deployed server, resulting in connection failures.

Consider one of the following solutions:

  • Remove this entry and configure it only in local development overrides
  • Use environment variables to conditionally enable/disable or redirect this MCP based on the deployment environment
  • Document clearly that this entry must be disabled before deployment
🤖 Prompt for AI Agents
In opencode.json around lines 3 to 7 the dev3000 MCP endpoint is hard-coded to
http://localhost:3684/mcp which will break in non-local deployments; remove this
entry from the committed config or replace it with an environment-driven toggle
(e.g., enable flag or URL read from env) so the localhost endpoint is only used
in local development, or add a clear comment/documentation requiring it be
disabled in production; implement the change by moving the dev3000 entry to a
local-only override or by switching the enabled/url values to reference
environment variables and ensuring defaults disable it for production.

Comment on lines +13 to +17
"shadcn": {
"type": "local",
"command": ["bunx --bun", "shadcn@latest", "mcp"],
"enabled": true
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

OpenCode AI opencode.json configuration schema local MCP command syntax

💡 Result:

Summary — opencode.json: local MCP server config

Required location/format

  • File: opencode.json or opencode.jsonc (project or ~/.config/opencode/opencode.json). [2]

MCP top-level key

  • mcp: object whose keys are arbitrary names for each MCP server. [1][2]

Local MCP server object fields

  • type (string, required): "local". [1]
  • command (array, required): command + args used to start the local MCP process (e.g. ["bun","x","my-mcp-command"] or ["npx","-y","@modelcontextprotocol/server-everything"]). [1][3]
  • enabled (boolean, optional): enable/disable server at startup (true/false). [1]
  • environment (object, optional): env vars passed when launching the command, e.g. { "MY_ENV_VAR": "value" }. [1]

Example
{
"$schema": "https://opencode.ai/config.json",
"mcp": {
"my-local-mcp-server": {
"type": "local",
"command": ["bun","x","my-mcp-command"],
"enabled": true,
"environment": { "MY_ENV_VAR": "my_value" }
}
}
}
(Example source: docs.) [1][2]

Notes

  • Multiple local and remote MCP entries may coexist; remote MCPs use type "remote" with url/headers instead. [1]
  • MCP names are referenced as tools in prompts (e.g., "use the mcp_everything tool"). [1]

Sources

  • OpenCode — MCP servers (config examples & options). [1]
  • OpenCode — Config (opencode.json format and locations). [2]

Correct the command array syntax to split arguments individually.

The command array ["bunx --bun", "shadcn@latest", "mcp"] violates the OpenCode configuration schema. According to the official documentation, each argument must be a separate array element: ["bunx", "--bun", "shadcn@latest", "mcp"]. The current syntax will fail to execute correctly.

🤖 Prompt for AI Agents
In opencode.json around lines 13 to 17 the "command" array combines the runner
and its flag into a single string ("bunx --bun"), which violates the OpenCode
schema; split each argument into its own array element so the command reads as
separate tokens (e.g., "bunx", "--bun", "shadcn@latest", "mcp") to match the
required schema and ensure correct execution.

Comment on lines +15 to +16
export default function ContactPage() {
const t = useTranslations("contact");
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Missing "use client" directive

This component uses useTranslations, which requires client-side context. Add the "use client" directive at the top of the file.

🔎 Proposed fix
+"use client";
+
 import Link from "next/link";
 import { useTranslations } from "next-intl";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export default function ContactPage() {
const t = useTranslations("contact");
"use client";
import Link from "next/link";
import { useTranslations } from "next-intl";
🤖 Prompt for AI Agents
In src/app/[locale]/(pages)/contact/page.tsx around lines 15 to 16, the
component uses useTranslations which requires client-side execution but the file
is missing the "use client" directive; add the line "use client" at the very top
of the file (before any imports) so the component runs as a client component and
retains existing imports/behavior.

Comment on lines +238 to +243
const handlePointerUp = (e: React.PointerEvent) => {
if (!draggable) return; // Release pointer capture
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);

isDragging.current = false;
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cursor not reset after drag ends.

When grabCursor is enabled, handlePointerDown sets cursor: "grabbing", but handlePointerUp never resets it. This leaves the cursor stuck in the "grabbing" state after the user releases.

🔎 Proposed fix
   const handlePointerUp = (e: React.PointerEvent) => {
     if (!draggable) return; // Release pointer capture
     (e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);

+    if (grabCursor) {
+      (e.currentTarget as HTMLElement).style.cursor = "";
+    }
+
     isDragging.current = 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.

Suggested change
const handlePointerUp = (e: React.PointerEvent) => {
if (!draggable) return; // Release pointer capture
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
isDragging.current = false;
};
const handlePointerUp = (e: React.PointerEvent) => {
if (!draggable) return; // Release pointer capture
(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId);
if (grabCursor) {
(e.currentTarget as HTMLElement).style.cursor = "";
}
isDragging.current = false;
};
🤖 Prompt for AI Agents
In src/components/fancy/blocks/simple-marquee.tsx around lines 238 to 243,
handlePointerUp currently releases pointer capture and clears isDragging but
does not restore the cursor; update it so when draggable is true and grabCursor
is enabled you reset the element's style.cursor back to "grab" (or to an empty
string when grabCursor is false) after releasing pointer capture so the cursor
is not left stuck on "grabbing".

tint = "#ffffff",
mouseReact = true,
mouseStrength = 0.2,
dpr = Math.min(window.devicePixelRatio || 1, 2),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

SSR crash: window is not defined during server-side rendering.

Accessing window.devicePixelRatio in the default parameter will throw a ReferenceError during server-side rendering. Even with "use client", Next.js may pre-render client components on the server where window is undefined.

🔎 Proposed fix

Move the default calculation inside the component or use a safe fallback:

-  dpr = Math.min(window.devicePixelRatio || 1, 2),
+  dpr,

Then inside the component, compute the actual value:

const actualDpr = dpr ?? (typeof window !== 'undefined' ? Math.min(window.devicePixelRatio || 1, 2) : 1);

And use actualDpr when creating the Renderer.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In src/components/FaultyTerminal.tsx around line 266, the default parameter uses
window.devicePixelRatio which crashes during SSR; remove any direct window
access from parameter defaults and instead compute the pixel ratio inside the
component at render time using a safe check (typeof window !== 'undefined') with
a fallback (e.g., 1) and clamp via Math.min(..., 2); then pass that computed
value (actualDpr) into the Renderer creation/usage so the server render path
never references window.

Comment on lines 1 to 43
{
"home": {
"creating-cool": "生活を豊かにする",
"makeLifeEasier": "を作っています。",
"stuff": "ヤツ",
"apps": "アプリ",
"tools": "ツール",
"bots": "Bot",
"mainBlurb1": "日常生活で出会う問題に対するオープンソースの解決策を作ります。",
"mainBlurb2": "大金持ちでなくても人生を豊かに。",
"takeLook": "見てみよう",
"learnMore": "もっと知る",
"infoTitle": "私達の秘訣",
"infoBlurb": "「The MikanDev Difference」",
"OSSonOSS": "オープンソース上のオープンソース",
"OSSonOSSBlurb": "私達はオープンソースの力を信じています。私達のプロジェクトは全て自己ホスト、修正、貢献が可能です。使用するサードパーティサービスもオープンソースです。",
"partOfPage": "このページもなんとオープンソースです!",
"viewOnGH": "GitHubで見てみる",
"SimpleNCheap": "シンプルさを追求し生まれた究極ののコストパフォマンス",
"SimpleNCheapBlurb": "私達は複雑なことを好みません。私達のプロジェクトは使いやすさを重視し、高額なサードパーティサービスに依存しません。その節約分はあなたに還元されます!",
"monthlyCost": "平均月額運用費用",
"MAUBilled": "従量課金サービス使用",
"despite": "にもかかわらず...",
"monthlyBandwidth": "平均月間帯域幅使用量",
"mainServices": "主要サービス",
"monthlyUptime": "平均月間稼働率",
"HowSoCheap": "なぜこんなに安いの?",
"SelfFunded": "100%自己資金",
"SelfFundedBlurb": "私達は投資家やVCに責任を負う必要がないため、利益を上げるタイミングを自分たちで決めることができます。私達はお金を稼ぐためではなく、面白いものを作るためにここにいます。",
"WorkWithBest": "最高の人たちと一緒に",
"WorkWithBestBlurb": "私達は他の素敵な組織と提携し、彼らの素晴らしいものを世に出すお手伝いをします。私達はコミュニティに全力を注いでいます!",
"workWithMe": "私たちと一緒にやろう!",
"NoWait": "これ以上言うことはありません。",
"NoWaitBlurb": "私たちのプロジェクトをチェックして、気に入るものがあるかどうかを見てください!"
},
"nav": {
"support": "サポート",
"docs": "ドキュメント",
"solutions": "ソリューション",
"legal": "法的条約",
"payments": "支払いセンター",
"blog": "ブログ",
"discord": "Discord",
"contact": "お問い合わせ",
"terms": "利用規約",
"privacy": "プライバシーポリシー",
"jp-payments": "特定商取引法に基づく表記",
"gdpr": "GDPRコンプライアンス",
"myAccount": "マイアカウント",
"resources": "リソース",
"cookieConsent": "そこの君!このサイトはCookieを使ってるぞ!!いいよね?",
"accept": "いいね!"
},
"contact": {
"title": "お問い合わせ",
"mail": "メール",
"phone": "電話 (日本のみ)*",
"discord": "Discord",
"join": "Discordに参加",
"general-support": "一般サポート",
"billing-support": "請求に関する問い合わせ",
"abuse-reports": "不正行為の報告",
"phone-disclaimer": "※日本国内の電話番号のみ対応しています。"
}
"home": {
"hero": {
"blurb1": "生活を豊かにする",
"blurb2": "いろいろな",
"blurb3": "ヤツらを作っています。",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "見てみよう"
},
"WorkWithBest": "最高の人たちと一緒に",
"WorkWithBestBlurb": "私達は他の素敵な組織と提携し、彼らの素晴らしいものを世に出すお手伝いをします。私達はコミュニティに全力を注いでいます!",
"workWithMe": "私たちと一緒にやろう!"
},
"nav": {
"support": "サポート",
"docs": "ドキュメント",
"solutions": "ソリューション",
"legal": "法的条約",
"payments": "支払いセンター",
"blog": "ブログ",
"discord": "Discord",
"contact": "お問い合わせ",
"terms": "利用規約",
"privacy": "プライバシーポリシー",
"jp-payments": "特定商取引法に基づく表記",
"gdpr": "GDPRコンプライアンス",
"myAccount": "マイアカウント",
"resources": "リソース",
"cookieConsent": "そこの君!このサイトはCookieを使ってるぞ!!いいよね?",
"accept": "いいね!"
},
"contact": {
"title": "お問い合わせ",
"mail": "メール",
"phone": "電話 (日本のみ)*",
"discord": "Discord",
"join": "Discordに参加",
"general-support": "一般サポート",
"billing-support": "請求に関する問い合わせ",
"abuse-reports": "不正行為の報告",
"phone-disclaimer": "※日本国内の電話番号のみ対応しています。"
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "*.json" -path "*/messages/*" | head -20

Repository: mikndotdev/mikn.dev

Length of output: 108


🏁 Script executed:

rg -n "useTranslations" --type=ts --type=tsx -B 2 -A 1 | head -100

Repository: mikndotdev/mikn.dev

Length of output: 90


🏁 Script executed:

cat ./src/messages/en.json

Repository: mikndotdev/mikn.dev

Length of output: 1367


🏁 Script executed:

rg "useTranslations" -A 1 | head -100

Repository: mikndotdev/mikn.dev

Length of output: 1288


🏁 Script executed:

cat ./src/messages/ja.json

Repository: mikndotdev/mikn.dev

Length of output: 1178


Add missing "layout" namespace to ja.json

The Japanese translation file uses a "nav" section but lacks the "layout" namespace that the application expects. The code explicitly calls useTranslations("layout") in template.tsx files, which will fail to retrieve Japanese translations. Rename "nav" to "layout" and ensure all keys match the en.json structure to provide complete translations for the Japanese locale.

🤖 Prompt for AI Agents
In src/messages/ja.json lines 1-43, the file uses a top-level "nav" namespace
but the app calls useTranslations("layout"); rename the "nav" key to "layout"
and move/merge its contents under that "layout" namespace, then compare against
src/messages/en.json and add or rename any missing keys so the Japanese layout
namespace matches the en.json structure (preserve existing Japanese strings,
ensure key names like cookieConsent/accept/myAccount/resources etc. match
exactly, and add any missing layout keys with appropriate Japanese translations
or placeholders).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (2)
src/messages/ja.json (1)

17-34: Missing layout namespace - still using nav

The Japanese translation file uses "nav" but the application calls useTranslations("layout") in template.tsx. This will cause Japanese translations to fail for layout keys. Additionally, ja.json is missing several keys that exist in en.json's layout section (e.g., home, vision, opensource).

Rename "nav" to "layout" and add the missing keys to match en.json.

src/app/[locale]/(homepage)/template.tsx (1)

17-17: Gate analytics behind user consent

useSwetrix is called unconditionally. To comply with privacy regulations (GDPR/CCPA), analytics should only initialise after the user consents via the ConsentManager. The codebase already has useConsentManager available for checking consent status.

🧹 Nitpick comments (3)
AGENTS.md (1)

43-43: Minor grammar refinement on line 43.

The phrasing "Prefer interfaces over types" would read more naturally as "Prefer interfaces to types" to align with standard English construction when using "prefer" with comparative elements.

🔎 Proposed refinement
- **Types:** Prefer interfaces over types for object definitions. Explicitly type component props.
+ **Types:** Prefer interfaces to types for object definitions. Explicitly type component props.
src/components/OSSProductCard.tsx (1)

31-42: Inline component created on every render

IconComponent is redefined on each render when icon is a string. This creates a new component identity each time, which can cause unnecessary re-renders and reconciliation work.

Consider extracting this logic or using conditional rendering directly in JSX.

🔎 Suggested refactor
-  const IconComponent =
-    typeof icon === "string"
-      ? () => (
-          <Image
-            src={icon}
-            alt={name}
-            width={48}
-            height={48}
-            className="rounded-lg"
-          />
-        )
-      : icon;
+  const Icon = icon;

Then in JSX:

         <div className="p-2 bg-muted/50 rounded-xl">
-          <IconComponent className="w-8 h-8 md:w-10 md:h-10 text-primary" />
+          {typeof icon === "string" ? (
+            <Image
+              src={icon}
+              alt={name}
+              width={48}
+              height={48}
+              className="rounded-lg w-8 h-8 md:w-10 md:h-10"
+            />
+          ) : (
+            <Icon className="w-8 h-8 md:w-10 md:h-10 text-primary" />
+          )}
         </div>
src/app/[locale]/(homepage)/page.tsx (1)

76-83: Empty className prop can be omitted

The className="" on CurvedLoop is redundant and can be removed for cleaner code.

       <CurvedLoop
         marqueeText={t("hero.loop")}
         speed={3}
         curveAmount={200}
         direction="right"
         interactive={true}
-        className=""
       />
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 26eeb87 and e0b968e.

⛔ Files ignored due to path filters (1)
  • src/assets/img/karasnap.png is excluded by !**/*.png
📒 Files selected for processing (9)
  • AGENTS.md
  • src/app/[locale]/(homepage)/page.tsx
  • src/app/[locale]/(homepage)/template.tsx
  • src/app/[locale]/page.tsx
  • src/components/CurvedLoop.tsx
  • src/components/DecryptedText.tsx
  • src/components/OSSProductCard.tsx
  • src/messages/en.json
  • src/messages/ja.json
💤 Files with no reviewable changes (1)
  • src/app/[locale]/page.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
src/app/[locale]/(homepage)/page.tsx (3)
src/components/vrm.tsx (1)
  • VRM (78-204)
src/components/DecryptedText.tsx (1)
  • DecryptedText (18-239)
src/components/OSSProductCard.tsx (1)
  • OSSProductCard (22-83)
src/app/[locale]/(homepage)/template.tsx (4)
src/components/CursorToys.tsx (1)
  • CursorToys (11-26)
src/components/mikn/Header.tsx (1)
  • Header (167-433)
src/components/SettingsController.tsx (1)
  • SettingsController (35-105)
src/components/mikn/Footer.tsx (1)
  • Footer (23-87)
src/components/OSSProductCard.tsx (1)
src/components/ui/card.tsx (5)
  • Card (77-77)
  • CardHeader (78-78)
  • CardContent (82-82)
  • CardTitle (80-80)
  • CardFooter (79-79)
🪛 LanguageTool
AGENTS.md

[grammar] ~43-~43: Consider using “to” with “prefer”.
Context: ...ic modules to be stricter. - Types: Prefer interfaces over types for object definitions. Explicitly type...

(PREFER_OVER_TO)

🔇 Additional comments (10)
AGENTS.md (1)

1-76: Comprehensive and well-structured operational guide.

This is a well-organised document that provides clear guidance on the project tech stack, build tooling, code style conventions, and expected agent behaviour. The coverage of Next.js 16 (App Router), TypeScript, Tailwind CSS 4, Three.js/R3F, and next-intl aligns well with the PR's modernisation objectives. The inclusion of explicit guidance on naming conventions, component libraries (Shadcn UI, Animate UI, Lucide), and Biome-based formatting will help maintain consistency. The note about strict: false in TypeScript is a pragmatic acknowledgement whilst still encouraging explicit prop typing.

Note: Line 12 lists "npm" as the package manager but may benefit from clarification, as section 2 emphasises Bun exclusively. Consider either updating line 12 to reflect Bun as the primary choice or expanding the note to clarify when npm is used (if at all).

src/components/OSSProductCard.tsx (1)

22-83: LGTM!

The component is well-structured with good TypeScript typing, responsive styling, and proper conditional rendering for the optional website URL. The card layout with header, content, and footer sections follows a clean pattern.

src/app/[locale]/(homepage)/template.tsx (1)

83-116: LGTM!

The layout composition is well-organised with proper component hierarchy (CursorToys → Header → children → SettingsController → Footer). The dynamic copyright year and structured navigation/footer links provide good maintainability.

src/app/[locale]/(homepage)/page.tsx (2)

24-27: LGTM!

Clean state management for coordinating the circular text animation with the letter animation completion. The showCircularText state pattern provides a nice sequential reveal effect.


28-179: LGTM!

The homepage composition is well-structured with clear visual sections (hero, OSS products, partners). Good use of responsive Tailwind classes (md:, lg: prefixes) and the animation components are appropriately integrated with scroll-triggered and view-triggered behaviours.

src/components/CurvedLoop.tsx (1)

71-91: LGTM!

The animation loop is well-implemented with proper cleanup via cancelAnimationFrame. The wrap-around logic correctly handles both directions, and the ready check prevents animation before text measurement completes.

src/components/DecryptedText.tsx (3)

18-31: LGTM!

Clean component API with sensible defaults. The props interface extends HTMLMotionProps<"span"> for proper type inference of spread props.


173-203: LGTM!

Good use of IntersectionObserver with proper cleanup. The hasAnimated state prevents re-triggering on subsequent intersections, which is the expected behaviour for a one-time view animation.


219-237: Good accessibility implementation

The sr-only span provides screen reader users with the display text, whilst the visual spans with aria-hidden="true" handle the scramble animation without confusing assistive technologies.

src/messages/en.json (1)

2-56: LGTM!

The JSON structure is now properly organised with distinct namespaces: layout for navigation/footer labels, home for homepage content (including nested ossProducts for product details), contact for contact page content, and top-level ossProducts for shared action button labels. The previous duplicate key issue has been resolved.

### General
- **Structure:** Follow the Next.js App Router structure (`src/app`).
- **Imports:** Use absolute imports with the `@/` alias (e.g., `import { Button } from "@/components/ui/button"`).
- **Sorting:** Imports are sorted automatically by Biome. Run `npm run format` to enforce this.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix package manager inconsistency.

Line 39 references npm run format, but section 2.2 and the introduction consistently emphasise using Bun. For consistency with the rest of the guide, this should be bun run format.

🔎 Proposed fix
- **Sorting:** Imports are sorted automatically by Biome. Run `npm run format` to enforce this.
+ **Sorting:** Imports are sorted automatically by Biome. Run `bun run format` to enforce this.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- **Sorting:** Imports are sorted automatically by Biome. Run `npm run format` to enforce this.
- **Sorting:** Imports are sorted automatically by Biome. Run `bun run format` to enforce this.
🤖 Prompt for AI Agents
In AGENTS.md around line 39, the docs reference "npm run format" which conflicts
with the guide's stated use of Bun; update that command to "bun run format" (or
the equivalent Bun script) so the package manager is consistent throughout the
document and run a quick pass to ensure no other npm commands remain in the
guide.

Comment on lines +123 to +127
const cursorStyle = interactive
? dragRef.current
? "grabbing"
: "grab"
: "auto";
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Cursor style won't update during drag

cursorStyle is computed using dragRef.current, but refs don't trigger re-renders. The cursor will always show "grab" even while dragging because the component doesn't re-render when dragRef.current changes.

Consider using state for the dragging flag if you want the visual cursor feedback:

🔎 Suggested fix
+  const [isDragging, setIsDragging] = useState(false);
   const dragRef = useRef(false);
   ...
   const onPointerDown = (e: PointerEvent) => {
     if (!interactive) return;
     dragRef.current = true;
+    setIsDragging(true);
     ...
   };
   ...
   const endDrag = () => {
     if (!interactive) return;
     dragRef.current = false;
+    setIsDragging(false);
     ...
   };
   ...
   const cursorStyle = interactive
-    ? dragRef.current
+    ? isDragging
       ? "grabbing"
       : "grab"
     : "auto";
🤖 Prompt for AI Agents
In src/components/CurvedLoop.tsx around lines 123 to 127, the cursorStyle is
derived from dragRef.current which is a ref and does not trigger re-renders, so
the cursor won't update while dragging; replace the ref-only check with a React
state boolean (e.g., isDragging) that you set true/false in your drag start/drag
end handlers (and any pointerup/pointercancel cleanup), then compute cursorStyle
from that state and ensure interactive still gates the behaviour; update any
existing event handlers to toggle the state and remove reliance on
dragRef.current for UI updates.

Comment on lines +2 to +16
"home": {
"hero": {
"blurb1": "生活を豊かにする",
"blurb2": "いろいろな",
"blurb3": "ヤツらを作っています。",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "見てみよう",
"loop": "私たちの良さげなところ"
},
"OpenSource": "すべてがオープンソース",
"OpenSourceBlurb": "私たちは自由とオープンさを信じています。私たちのプロジェクトやサービスはすべてオープンソースで、誰でも使用、修正、配布が可能です。私たちのGitHubを是非見てみてね!",
"WorkWithBest": "最高の人たちと一緒に",
"WorkWithBestBlurb": "私達は他の素敵な組織と提携し、彼らの素晴らしいものを世に出すお手伝いをします。私達はコミュニティに全力を注いでいます!",
"workWithMe": "私たちと一緒にやろう!"
},
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Missing nested ossProducts translations under home

The English file defines home.ossProducts.karaSnap and home.ossProducts.nextDiscordAuth with title and description keys, which are used in page.tsx. The Japanese file is missing these entries, so Japanese users will see untranslated product information.

🔎 Suggested addition
   "home": {
     "hero": {
       ...
     },
     ...
-    "workWithMe": "私たちと一緒にやろう!"
+    "workWithMe": "私たちと一緒にやろう!",
+    "ossProducts": {
+      "karaSnap": {
+        "title": "KaraSnap",
+        "description": "AIを活用した、最も簡単なカラオケ記録アプリ"
+      },
+      "nextDiscordAuth": {
+        "title": "Next Discord Auth",
+        "description": "Next.jsアプリ向けのシンプルなDiscord OAuth"
+      }
+    }
   },
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"home": {
"hero": {
"blurb1": "生活を豊かにする",
"blurb2": "いろいろな",
"blurb3": "ヤツらを作っています。",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "見てみよう",
"loop": "私たちの良さげなところ"
},
"OpenSource": "すべてがオープンソース",
"OpenSourceBlurb": "私たちは自由とオープンさを信じています。私たちのプロジェクトやサービスはすべてオープンソースで、誰でも使用、修正、配布が可能です。私たちのGitHubを是非見てみてね!",
"WorkWithBest": "最高の人たちと一緒に",
"WorkWithBestBlurb": "私達は他の素敵な組織と提携し、彼らの素晴らしいものを世に出すお手伝いをします。私達はコミュニティに全力を注いでいます!",
"workWithMe": "私たちと一緒にやろう!"
},
"home": {
"hero": {
"blurb1": "生活を豊かにする",
"blurb2": "いろいろな",
"blurb3": "ヤツらを作っています。",
"circularText": "Open Source ☆ Sugoi ☆ MikanDev ☆ Freedom ☆ ",
"primaryCta": "見てみよう",
"loop": "私たちの良さげなところ"
},
"OpenSource": "すべてがオープンソース",
"OpenSourceBlurb": "私たちは自由とオープンさを信じています。私たちのプロジェクトやサービスはすべてオープンソースで、誰でも使用、修正、配布が可能です。私たちのGitHubを是非見てみてね!",
"WorkWithBest": "最高の人たちと一緒に",
"WorkWithBestBlurb": "私達は他の素敵な組織と提携し、彼らの素晴らしいものを世に出すお手伝いをします。私達はコミュニティに全力を注いでいます!",
"workWithMe": "私たちと一緒にやろう!",
"ossProducts": {
"karaSnap": {
"title": "KaraSnap",
"description": "AIを活用した、最も簡単なカラオケ記録アプリ"
},
"nextDiscordAuth": {
"title": "Next Discord Auth",
"description": "Next.jsアプリ向けのシンプルなDiscord OAuth"
}
}
},
🤖 Prompt for AI Agents
In src/messages/ja.json around lines 2 to 16, the home.ossProducts object is
missing (specifically home.ossProducts.karaSnap and
home.ossProducts.nextDiscordAuth with title and description), causing
untranslated product info; add a home.ossProducts object under the existing home
block containing karaSnap and nextDiscordAuth entries, each with title and
description keys translated into Japanese, matching the exact key names used in
the English file so page.tsx can read them; ensure valid JSON structure and
proper commas.

@maamokun
Copy link
Collaborator Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Dec 27, 2025

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@opencode-agent
Copy link

opencode-agent bot commented Jan 8, 2026

remote: Duplicate header: "Authorization"
fatal: unable to access 'https://github.com/mikndotdev/mikn.dev/': The requested URL returned error: 400

opencode session  |  github run

@opencode-agent
Copy link

opencode-agent bot commented Jan 8, 2026

remote: Duplicate header: "Authorization"
fatal: unable to access 'https://github.com/mikndotdev/mikn.dev/': The requested URL returned error: 400

opencode session  |  github run

@github-actions
Copy link

github-actions bot commented Jan 8, 2026

Kyaaa~! I've reviewed your massive refactor project, Master! bows It's quite impressive how much you've modernized everything! >w<

Here's my analysis of the code changes, nya:

🚨 Critical Issues

1. Static Export Incompatibility

The next.config.mjs has output: "export" set, which means your app is trying to build as a static site. However, src/app/[locale]/page.tsx exports an async server component - this cannot work with static export mode! Static exports require all rendering to happen at build time, but async server components need to be rendered on the server.

Fix options:

  • Option A: Remove async from the page component and make it a standard client component
  • Option B: Remove output: "export" from next.config.mjs if you need SSR
  • Option C: Use getStaticProps for pages that need data fetching

2. Analytics Privacy Concern ⚠️

In src/app/[locale]/template.tsx, useSwetrix is called unconditionally without checking for user consent! This violates privacy regulations (GDPR, CCPA). You should gate analytics initialization behind the consent manager:

const { hasConsented } = useConsentManager();

useEffect(() => {
  if (hasConsented()) {
    useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" });
  }
}, [hasConsented]);

3. Memory Leaks in WebGL Components

Several components (SplashCursor.tsx, vrm.tsx, FluidGlass.tsx) have potential memory leaks:

  • SplashCursor.tsx: Missing cleanup for event listeners and shader programs
  • vrm.tsx: VRM resources aren't being disposed on unmount; loaders can resolve after unmount
  • FluidGlass.tsx: FBOs and textures aren't being cleaned up when components are remounted

4. Type Safety Issues

  • React types mismatch: You're using React 19.2.0 but @types/react is pinned to ^18
  • Strict mode disabled: "strict": false in tsconfig.json defeats the purpose of TypeScript
  • Unsafe type casts: Lots of as any casts that bypass type checking

5. Accessibility & ARIA 🎨

Multiple components have missing or incorrect ARIA attributes:

  • Social links without aria-label
  • Buttons using icons without aria-label for screen readers
  • Settings panel using <div> instead of semantic dialog element

6. Code Quality & Performance 💡

  • Unused imports: Several components have dead imports that should be cleaned up
  • Bundle size: Some components (like SplashCursor at 1521 lines!) are very large and could hurt performance
  • Event listener leaks: Multiple components don't properly clean up event listeners

✨ Recommendations

High Priority

  1. Fix static export compatibility - Choose either remove static export or convert async pages to data-fetching client components
  2. Gate analytics behind consent - Add privacy checks before initializing Swetrix
  3. Add WebGL cleanup - Implement proper dispose patterns in all Three.js/VRM components
  4. Fix React types - Update @types/react to match React 19
  5. Enable strict mode - Gradually fix type errors from enabling strict mode in tsconfig

Medium Priority

  1. Fix accessibility issues - Add proper ARIA labels to all interactive elements
  2. Clean up unused imports - Remove dead code throughout the codebase
  3. Reduce bundle sizes - Split large components (SplashCursor at 1521 lines is huge!)
  4. Optimize performance - Add proper event listener cleanup in all canvas/WebGL components

Low Priority

  1. Add input validation - Validate environment variables exist before using them
  2. Remove duplicate utilities - You have src/components/cn.ts AND src/lib/utils.ts with identical cn() functions
  3. Standardize error handling - Add consistent try/catch blocks and error logging
  4. Consider dynamic imports - Use next/dynamic for heavy components like SplashCursor to avoid bloating the initial bundle

🌸 Overall Assessment

This is a massive and impressive refactor, Master! You've successfully migrated from a Cloudflare Workers deployment to a modern Next.js architecture, replaced the entire UI system, and added lots of fun interactive features. The code quality is generally good with proper TypeScript typing and modern React patterns.

However, there are some critical issues that need addressing before merging:

  • Static export vs async components conflict
  • Analytics privacy compliance
  • Memory leaks in WebGL components
  • Type mismatches

Once you fix these issues, your codebase will be much more robust and production-ready! Keep up the great work, nya! hugs

Let me know if you'd like me to dive deeper into any specific files or create more detailed fix suggestions!I've reviewed your massive refactor, Master! bows It's quite impressive how much you've modernized everything, nya! >w<

🚨 Critical Issues Found

1. Static Export Incompatibility

  • next.config.mjs has output: "export" but src/app/[locale]/page.tsx is an async server component - these don't work together! Static export requires all rendering at build-time.

2. Analytics Privacy Violation ⚠️

src/app/[locale]/template.tsx calls useSwetrix unconditionally without user consent - violates GDPR/CCPA. Gate it behind useConsentManager()!

3. Memory Leaks

  • SplashCursor.tsx, vrm.tsx, FluidGlass.tsx - Missing cleanup for WebGL shaders/textures, event listeners, and animation loops on unmount

4. Type Mismatches

  • React 19.2.0 installed but @types/react is pinned to ^18
  • tsconfig.json has "strict": false - disables TypeScript's safety checks!

5. Accessibility Issues

  • Missing aria-label on social links (src/components/mikn/Footer.tsx)
  • Icons used as buttons without ARIA labels (src/components/vrm.tsx)
  • Settings panel uses <div> instead of <dialog> (src/components/SettingsController.tsx)

6. Bundle Size

  • SplashCursor.tsx is 1521 lines! Consider code-splitting

✨ Recommendations

High Priority:

  1. Fix static export - remove async from pages or disable output: "export"
  2. Gate analytics - add hasConsented() check before useSwetrix() call
  3. Fix React types - update to @types/react: ^19
  4. Enable strict mode - change "strict": false to true gradually
  5. Fix WebGL memory leaks - add cleanup functions to useEffect returns in all Three.js components

Medium Priority:
6. Fix accessibility - add aria-label to all interactive elements
7. Clean unused imports
8. Optimize performance - proper event listener cleanup

The code quality is solid overall, but these issues need attention before production, nya! hugs

New%20session%20-%202026-01-08T16%3A24%3A54.819Z
opencode session  |  github run

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants