-
-
Couldn't load subscription status.
- Fork 0
Refactor #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughA major architectural refactor transitioning from Cloudflare/AWS infrastructure to @c15t framework, removing legacy page components, simplifying internationalisation, and introducing an interactive cursor effects system with new UI component library featuring advanced animations and 3D rendering capabilities. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Browser
participant SettingsController
participant CursorToysContext
participant CursorToys
participant CursorEffect[Cursor Effect Component]
User->>SettingsController: Click cursor toy button
SettingsController->>CursorToysContext: setSelectedToy("spark"|"splash"|...)
CursorToysContext->>CursorToysContext: Update state & localStorage
CursorToysContext->>CursorToys: Re-render with new selectedToy
CursorToys->>CursorEffect: Render selected effect component
CursorEffect->>Browser: Mount/activate cursor effect
User->>Browser: Move mouse / Click
Browser->>CursorEffect: Fire mouse/click events
CursorEffect->>CursorEffect: Update animation state
CursorEffect->>Browser: Render canvas/DOM updates
sequenceDiagram
participant App
participant Layout["[locale]/layout.tsx"]
participant Template["[locale]/template.tsx"]
participant ConsentManager
participant CursorToysProvider
App->>Layout: Validate locale & render
alt Locale not in routing.locales
Layout->>App: notFound()
else Locale valid
Layout->>Template: Render with children
Template->>ConsentManager: Wrap children
ConsentManager->>ConsentManager: Initialize @c15t provider
Template->>CursorToysProvider: Initialize cursor toys
CursorToysProvider->>CursorToysProvider: Load selectedToy from localStorage
CursorToysProvider->>App: Provide context to children
end
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Areas requiring extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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. Comment |
Summary of ChangesHello @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 ( Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. 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
|
|
Warning Gemini encountered an error creating the review. You can try again by commenting |
Deploying mikn-dev with
|
| Latest commit: |
e04bede
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://07292e47.mikn-dev.pages.dev |
| Branch Preview URL: | https://refactor.mikn-dev.pages.dev |
There was a problem hiding this 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: RemoverenderToStringimport from client component and use a browser-compatible alternative.
renderToStringis 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 usingcreateRootwith 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"andaria-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, andtargetRotationobjects. 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
rotationRefand 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: returnuseMediadirectly; 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 hereAlso 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: Guardrepeatand respect reduced motionPrevent 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 duplicationFile name is “marqee.tsx”. Consider renaming to “marquee.tsx” and exporting from an index to avoid breaking imports. Also note two
cnutilities 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: Usenext/linkfor 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 propindexfromMobileMenuItemPropsand call sites
indexisn’t used insideMobileMenuItem. 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 imageUse
next/imagefor better performance and add width/height/alt. If sticking to<img>, addloading="lazy"anddecoding="async".
175-175: Avoid magic header heights; derive from ref
headerHeight = 88andhide: { top: -88 }can drift from actual layout (h-12 / lg:h-16). MeasureoffsetHeightvia a ref and compute dynamically.Also applies to: 87-95
179-186: Throttle scroll updates to reduce re-render churnScroll 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; returnchildrendirectly.-"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 <= 0to 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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockbun.lockbis 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/**/*.tsensures 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
offlinemode 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
scriptsandcallbacksplaceholders 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
verbatimModuleSyntaxenabled (it defaults to false), andesModuleInterop: truewithmoduleResolution: "bundler"already permits mixed value and type imports from the same module. The codebase usesimport typepatterns successfully elsewhere without.jsextensions, which is the established convention here.The
@ts-ignoreon line 4 may still warrant investigation—it could indicate an untyped export fromthree/examples/jsm/loaders/GLTFLoader—but the recommendation to add.jsextensions 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.
| <?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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <?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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| "files": { | ||
| "ignoreUnknown": true, | ||
| "includes": ["**", "!node_modules", "!.next", "!dist", "!build"] | ||
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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.
| { | ||
| "$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": {} | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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"
fiLength 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.
| href={"https://youtube.com/@mikndotdev"} | ||
| target="_blank" | ||
| className={"block w-full h-full"} | ||
| > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
target="_blank" without rel is a security risk (reverse‑tabnabbing).
Add rel="noopener noreferrer" to external links opened in a new tab.
Apply:
- <a
- href={"https://youtube.com/@mikndotdev"}
- target="_blank"
- className={"block w-full h-full"}
- >
+ <a
+ href="https://youtube.com/@mikndotdev"
+ target="_blank"
+ rel="noopener noreferrer"
+ className="block w-full h-full"
+ >📝 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.
| href={"https://youtube.com/@mikndotdev"} | |
| target="_blank" | |
| className={"block w-full h-full"} | |
| > | |
| href="https://youtube.com/@mikndotdev" | |
| target="_blank" | |
| rel="noopener noreferrer" | |
| className="block w-full h-full" | |
| > |
🤖 Prompt for AI Agents
In src/components/vrm.tsx around lines 165 to 168, the anchor uses
target="_blank" without rel attributes which risks reverse-tabnabbing; update
the <a> to include rel="noopener noreferrer" (i.e., add rel="noopener
noreferrer" alongside target="_blank") so external links opened in a new tab are
safe.
| <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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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.
src/i18n/request.ts
Outdated
| if (!locale || !routing.locales.includes(locale as any)) { | ||
| locale = routing.defaultLocale; | ||
| } |
There was a problem hiding this comment.
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.
src/i18n/request.ts
Outdated
| 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]; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| export function cn(...inputs: ClassValue[]) { | ||
| return twMerge(clsx(inputs)); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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-cloudflareandwranglerwhich are no longer in the dependencies and will fail when executed.
53-53: Update React types to match React 19.The
@types/reactversion (^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
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.locksrc/assets/cloudfront.pngis 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
| const optimizerUrl = process.env.NEXT_PUBLIC_IMAGE_OPTIMIZER_URL || ""; | ||
| const hostname = process.env.CF_PAGES_URL; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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("&")}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix URL construction to handle edge cases.
The URL construction has several potential issues:
CF_PAGES_URLmay not include a protocol (e.g., "example.com" instead of "https://example.com")- Concatenating
${hostname}${src}assumeshostnamedoesn't end with/andsrcstarts with/ - 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.
| output: "export", | ||
| images: { | ||
| loader: "custom", | ||
| loaderFile: "./imgLoader.ts", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
There was a problem hiding this 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
tis 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 causelocaleto beundefinedat 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+,
paramsis 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
⛔ Files ignored due to path filters (1)
bun.lockis 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.
| 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, | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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:
- Removing the
[locale]dynamic route segment entirely, or - 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.
| 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, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
There was a problem hiding this 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:strictmode remains disabled.This mirrors the concern raised in the previous review. Setting
"strict": falsedisables 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.,noImplicitAnyorstrictNullChecks) 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 readlocalefrom 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: StrengthenModePropstyping and drop broadRecord<string, unknown>Current typing weakens safety (e.g.
navItemsis cast later). Define a concrete shape for material knobs +navItemsto 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 flagsThis 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 hitchProactively 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 frameworksDirectly mutating
window.locationbypasses client routing. Consider your router (next/router,react-router) for smoother transitions and prefetch.
63-69:<Scroll html />with no childrenThis mounts an empty HTML overlay. Remove if unused.
src/interfaces/CursorToys.ts (1)
3-6: Name the props and export the union for reusePrefer 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 messagesSmall 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 castThe 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
isCheckedcan 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
getVariantscalls a hook from a plain function (eslint disabled). Prefer a hook wrapper or passanimationTypein.-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
requestAnimationFrameor 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
optionsas 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 usehsr.className. Either removevariableor 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 .ideasrc/components/animate-ui/primitives/buttons/button.tsx (2)
18-33: Forward ref for focus and integrationsExpose 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 asChildButtonProps 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 panelImprove 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 componentchildren 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 writelocalStorage.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 awaitNext.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
⛔ Files ignored due to path filters (1)
bun.lockis 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 narrowedexcludedoes not inadvertently include build artefacts.The updated
includepaths appropriately prioritise Next.js-generated types. However, theexcludearray has been narrowed to only["node_modules"], removing previous broader exclusions (including legacysst.config.ts). This risks unintentionally including build output directories (e.g.,.nextcompiled output,dist,build,coverage,out) if they exist. Whilst.nextis 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 supportedTroika's
outlineBlurprop 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/dreiAPIThe code properly matches the documented API.
bufferis correctly instantiated viauseFBO()(line 68), andbuffer.texture(aTHREE.Texture) is appropriately passed toMeshTransmissionMaterial'sbufferprop 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 intentionalThe 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.tsxhas"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
GlobalCursorToysis never imported or rendered—it exists only as a definition insrc/components/GlobalCursorToys.tsx. TheCursorToysProvideris correctly wrapping the application insrc/app/layout.tsx, and theuseCursorToys()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 compatibilityWhen 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 logicThe conditional rendering is clear and keeps children always present. No issues spotted.
src/app/[locale]/template.tsx (1)
10-15: Provider correctly wraps the templateThe 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.
| </Footer> | ||
| </> | ||
| ); | ||
| useSwetrix("XxNIMaHCaVG3", { apiURL: "https://analytics.mikandev.tech/log" }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Gate analytics behind user consent
useSwetrix is called unconditionally. To meet privacy expectations, initialise analytics only after the user consents (via ConsentManager). Wire this to the consent state before calling useSwetrix.
| <html className={hsr.className}> | ||
| <body> | ||
| <CursorToysProvider>{children}</CursorToysProvider> | ||
| </body> | ||
| </html> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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 }))} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| <AnimateIconContext.Provider | ||
| value={{ | ||
| controls, | ||
| animation: currentAnimation, | ||
| loop, | ||
| loopDelay, | ||
| active: localAnimate, | ||
| animate, | ||
| initialOnAnimateEnd, | ||
| completeOnStop, | ||
| delay, | ||
| }} | ||
| > | ||
| {content} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| <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.
| 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> | ||
| ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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.
| 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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
| 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).
| // 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), | ||
| ); | ||
|
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Summary by CodeRabbit
Release Notes
New Features
Bug Fixes & Updates
Removals