Conversation
|
You've used up your 5 PR reviews for this month under the Korbit Starter Plan. You'll get 5 more reviews on November 11th, 2025 or you can upgrade to Pro for unlimited PR reviews and enhanced features in your Korbit Console. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
WalkthroughReplaces the previous landing page with a component-driven layout: adds a new home page under Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Browser
participant App as app/layout.tsx
participant ThemeProv as ThemeProvider
participant Navbar as Navbar
participant CardNav as CardNav
participant Toggle as ModeToggle
Browser->>App: Request page
App->>ThemeProv: Wrap children
ThemeProv-->>App: Theme context provided
App->>Navbar: Render header
Navbar->>CardNav: Mount default items
Navbar->>Toggle: Mount mode toggle
User->>Toggle: Click
rect rgb(215,230,255)
Note over Toggle: View Transition circular reveal
Toggle->>ThemeProv: setTheme('dark'/'light')
ThemeProv->>App: Apply CSS variables
end
User->>CardNav: Click hamburger
rect rgb(235,245,220)
Note over CardNav: GSAP timeline animates height + cards
CardNav->>CardNav: Play timeline (staggered entrance)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches🧪 Generate unit tests (beta)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (1)
⏰ 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). (2)
🔇 Additional comments (2)
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 |
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
portfolio | c629787 | Commit Preview URL Branch Preview URL |
Nov 07 2025, 01:27 AM |
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
app/layout.tsx (1)
27-28: Remove the stray template literal
Line 28 contains ``````;`` which makes the layout fail to compile. Drop the extra literal so the file parses again.-export const metadata: Metadata = { - title: "Clerk Next.js Quickstart", - description: "Generated by create next app", -}; -``; +export const metadata: Metadata = { + title: "Clerk Next.js Quickstart", + description: "Generated by create next app", +};
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (11)
app/(pages)/page.tsx(1 hunks)app/globals.css(1 hunks)app/layout.tsx(2 hunks)app/page.tsx(0 hunks)components.json(1 hunks)components/shared/mode-toggle.tsx(1 hunks)components/shared/navbar.tsx(1 hunks)components/shared/theme-provider.tsx(1 hunks)components/ui/card-nav.tsx(1 hunks)lib/utils.ts(1 hunks)package.json(2 hunks)
💤 Files with no reviewable changes (1)
- app/page.tsx
🧰 Additional context used
🧬 Code graph analysis (3)
components/shared/mode-toggle.tsx (1)
lib/utils.ts (1)
cn(4-6)
components/ui/card-nav.tsx (2)
lib/utils.ts (1)
cn(4-6)components/shared/mode-toggle.tsx (1)
ModeToggle(15-114)
app/layout.tsx (2)
components/shared/theme-provider.tsx (1)
ThemeProvider(6-11)components/shared/navbar.tsx (1)
Navbar(10-16)
🪛 Biome (2.1.2)
app/globals.css
[error] 126-126: Unknown type selector is not allowed.
See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.
(lint/correctness/noUnknownTypeSelector)
[error] 127-127: Unknown type selector is not allowed.
See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.
(lint/correctness/noUnknownTypeSelector)
⏰ 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). (2)
- GitHub Check: deploy
- GitHub Check: Workers Builds: portfolio
🔇 Additional comments (1)
lib/utils.ts (1)
4-6: Utility helper aligns with Tailwind merging.Implementation matches the established clsx + twMerge pattern and looks good to ship.
| ::view-transition-old(root), | ||
| ::view-transition-new(root) { | ||
| animation: none; | ||
| mix-blend-mode: normal; | ||
| } |
There was a problem hiding this comment.
Address Biome lint errors on view transition selectors.
Biome 2.1.2 flags both ::view-transition-old(root) and ::view-transition-new(root) as unknown type selectors, which will keep the lint step red. Split the rule and add targeted biome-ignore comments (or configure the rule) so the pipeline passes while retaining the transition override.
Apply this diff to silence the rule without changing behavior:
-/* Disable transition blending between views */
-::view-transition-old(root),
-::view-transition-new(root) {
- animation: none;
- mix-blend-mode: normal;
-}
+/* Disable transition blending between views */
+/* biome-ignore lint/correctness/noUnknownTypeSelector: View transition pseudo-element */
+::view-transition-old(root) {
+ animation: none;
+ mix-blend-mode: normal;
+}
+
+/* biome-ignore lint/correctness/noUnknownTypeSelector: View transition pseudo-element */
+::view-transition-new(root) {
+ animation: none;
+ mix-blend-mode: normal;
+}🧰 Tools
🪛 Biome (2.1.2)
[error] 126-126: Unknown type selector is not allowed.
See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.
(lint/correctness/noUnknownTypeSelector)
[error] 127-127: Unknown type selector is not allowed.
See MDN web docs for more details.
Consider replacing the unknown type selector with valid one.
(lint/correctness/noUnknownTypeSelector)
🤖 Prompt for AI Agents
In app/globals.css around lines 126 to 130, Biome flags the combined selectors
::view-transition-old(root) and ::view-transition-new(root) as unknown type
selectors; split the combined rule into two separate rules (one for each
pseudo-element) and add a targeted biome-ignore comment above each selector
(e.g., /* biome-ignore-next-line no-unknown-type-selector */) so linting is
silenced while keeping animation:none and mix-blend-mode:normal for both; ensure
no behavioral changes by preserving the same declarations in each rule.
| const transition = document.startViewTransition(() => { | ||
| flushSync(() => { | ||
| stableSetTheme.current(newTheme); | ||
| }); | ||
| }); | ||
|
|
||
| await transition.ready; | ||
|
|
||
| const { top, left, width, height } = | ||
| buttonRef.current.getBoundingClientRect(); | ||
| const x = left + width / 2; | ||
| const y = top + height / 2; | ||
| const endRadius = Math.hypot( | ||
| Math.max(x, window.innerWidth - x), | ||
| Math.max(y, window.innerHeight - y) | ||
| ); | ||
|
|
||
| document.documentElement.animate( | ||
| { | ||
| clipPath: [ | ||
| `circle(0px at ${x}px ${y}px)`, | ||
| `circle(${endRadius}px at ${x}px ${y}px)`, | ||
| ], | ||
| }, | ||
| { | ||
| duration, | ||
| easing: "cubic-bezier(0.4, 0, 0.2, 1)", | ||
| pseudoElement: "::view-transition-new(root)", | ||
| } | ||
| ); | ||
| }, [resolvedTheme, duration, mounted]); |
There was a problem hiding this comment.
Guard document.startViewTransition for unsupported browsers.
document.startViewTransition only ships in Chromium today; on Safari/Firefox this click handler throws, so the theme toggle never completes. Add a feature check and fall back to a simple setTheme when the API isn’t present.
Apply this diff to handle browsers without the View Transition API:
- const transition = document.startViewTransition(() => {
- flushSync(() => {
- stableSetTheme.current(newTheme);
- });
- });
-
- await transition.ready;
+ const startViewTransition = (document as Document & {
+ startViewTransition?: (
+ updateCallback: () => void
+ ) => { ready: Promise<unknown> };
+ }).startViewTransition;
+
+ if (!startViewTransition) {
+ stableSetTheme.current(newTheme);
+ return;
+ }
+
+ const transition = startViewTransition(() => {
+ flushSync(() => {
+ stableSetTheme.current(newTheme);
+ });
+ });
+
+ await transition.ready;📝 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 transition = document.startViewTransition(() => { | |
| flushSync(() => { | |
| stableSetTheme.current(newTheme); | |
| }); | |
| }); | |
| await transition.ready; | |
| const { top, left, width, height } = | |
| buttonRef.current.getBoundingClientRect(); | |
| const x = left + width / 2; | |
| const y = top + height / 2; | |
| const endRadius = Math.hypot( | |
| Math.max(x, window.innerWidth - x), | |
| Math.max(y, window.innerHeight - y) | |
| ); | |
| document.documentElement.animate( | |
| { | |
| clipPath: [ | |
| `circle(0px at ${x}px ${y}px)`, | |
| `circle(${endRadius}px at ${x}px ${y}px)`, | |
| ], | |
| }, | |
| { | |
| duration, | |
| easing: "cubic-bezier(0.4, 0, 0.2, 1)", | |
| pseudoElement: "::view-transition-new(root)", | |
| } | |
| ); | |
| }, [resolvedTheme, duration, mounted]); | |
| const startViewTransition = (document as Document & { | |
| startViewTransition?: ( | |
| updateCallback: () => void | |
| ) => { ready: Promise<unknown> }; | |
| }).startViewTransition; | |
| if (!startViewTransition) { | |
| stableSetTheme.current(newTheme); | |
| return; | |
| } | |
| const transition = startViewTransition(() => { | |
| flushSync(() => { | |
| stableSetTheme.current(newTheme); | |
| }); | |
| }); | |
| await transition.ready; | |
| const { top, left, width, height } = | |
| buttonRef.current.getBoundingClientRect(); | |
| const x = left + width / 2; | |
| const y = top + height / 2; | |
| const endRadius = Math.hypot( | |
| Math.max(x, window.innerWidth - x), | |
| Math.max(y, window.innerHeight - y) | |
| ); | |
| document.documentElement.animate( | |
| { | |
| clipPath: [ | |
| `circle(0px at ${x}px ${y}px)`, | |
| `circle(${endRadius}px at ${x}px ${y}px)`, | |
| ], | |
| }, | |
| { | |
| duration, | |
| easing: "cubic-bezier(0.4, 0, 0.2, 1)", | |
| pseudoElement: "::view-transition-new(root)", | |
| } | |
| ); | |
| }, [resolvedTheme, duration, mounted]); |
🤖 Prompt for AI Agents
In components/shared/mode-toggle.tsx around lines 43 to 73, the handler calls
document.startViewTransition which throws on browsers that don't support the
View Transition API; add a feature check and fallback so the toggle still
completes: detect if document.startViewTransition is a function and, if not,
call flushSync(() => stableSetTheme.current(newTheme)) (or setTheme directly)
and return early; if it exists, run the existing startViewTransition block and
await transition.ready as before, and only run the
document.documentElement.animate call when the API was used.
| const setCardRef = (i: number) => (el: HTMLDivElement | null) => { | ||
| if (el) cardsRef.current[i] = el; | ||
| }; |
There was a problem hiding this comment.
Clear stale card refs when items unmount
When a card unmounts, React calls the ref callback with null, but Line 159 exits early so the old DOM node stays in cardsRef.current. GSAP still tries to animate that stale element on the next open, which can throw or at least desync the animation once consumers pass dynamic items. Please clear the entry when el is null.
- const setCardRef = (i: number) => (el: HTMLDivElement | null) => {
- if (el) cardsRef.current[i] = el;
- };
+ const setCardRef = (i: number) => (el: HTMLDivElement | null) => {
+ if (el) {
+ cardsRef.current[i] = el;
+ } else {
+ cardsRef.current.splice(i, 1);
+ }
+ };Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In components/ui/card-nav.tsx around lines 158 to 160, the ref callback only
assigns the element when el is truthy so stale DOM nodes remain in
cardsRef.current when a card unmounts; change the callback so it clears the slot
when el is null (e.g., set cardsRef.current[i] = null or delete
cardsRef.current[i]) instead of returning early, ensuring the stale ref is
removed so GSAP won't try to animate an unmounted element.
| <div | ||
| className={cn( | ||
| "hamburger-menu group h-full flex flex-col items-center justify-center cursor-pointer gap-1.5 order-2 md:order-0 transition-colors", | ||
| isHamburgerOpen ? "open" : "" | ||
| )} | ||
| onClick={toggleMenu} | ||
| onKeyDown={(e) => e.key === "Enter" && toggleMenu()} | ||
| role="button" | ||
| aria-label={isExpanded ? "Close menu" : "Open menu"} | ||
| tabIndex={0} | ||
| > | ||
| <div | ||
| className={cn( | ||
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | ||
| isHamburgerOpen && "translate-y-1 rotate-45" | ||
| )} | ||
| /> | ||
| <div | ||
| className={cn( | ||
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | ||
| isHamburgerOpen && "-translate-y-1 -rotate-45" | ||
| )} | ||
| /> | ||
| </div> |
There was a problem hiding this comment.
Use a real <button> and expose state for screen readers
The hamburger trigger on Line 179 is a div with role="button", but it only listens for Enter and omits Space and aria-expanded, so keyboard and assistive tech users in the United States cannot reliably operate it. Swap to a native <button> and surface the open state.
- <div
+ <button
className={cn(
"hamburger-menu group h-full flex flex-col items-center justify-center cursor-pointer gap-1.5 order-2 md:order-0 transition-colors",
isHamburgerOpen ? "open" : ""
)}
- onClick={toggleMenu}
- onKeyDown={(e) => e.key === "Enter" && toggleMenu()}
- role="button"
+ type="button"
+ onClick={toggleMenu}
aria-label={isExpanded ? "Close menu" : "Open menu"}
- tabIndex={0}
+ aria-expanded={isExpanded}
>
<div
className={cn(
"hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75",
isHamburgerOpen && "translate-y-1 rotate-45"
)}
/>
<div
className={cn(
"hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75",
isHamburgerOpen && "-translate-y-1 -rotate-45"
)}
/>
- </div>
+ </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.
| <div | |
| className={cn( | |
| "hamburger-menu group h-full flex flex-col items-center justify-center cursor-pointer gap-1.5 order-2 md:order-0 transition-colors", | |
| isHamburgerOpen ? "open" : "" | |
| )} | |
| onClick={toggleMenu} | |
| onKeyDown={(e) => e.key === "Enter" && toggleMenu()} | |
| role="button" | |
| aria-label={isExpanded ? "Close menu" : "Open menu"} | |
| tabIndex={0} | |
| > | |
| <div | |
| className={cn( | |
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | |
| isHamburgerOpen && "translate-y-1 rotate-45" | |
| )} | |
| /> | |
| <div | |
| className={cn( | |
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | |
| isHamburgerOpen && "-translate-y-1 -rotate-45" | |
| )} | |
| /> | |
| </div> | |
| <button | |
| className={cn( | |
| "hamburger-menu group h-full flex flex-col items-center justify-center cursor-pointer gap-1.5 order-2 md:order-0 transition-colors", | |
| isHamburgerOpen ? "open" : "" | |
| )} | |
| type="button" | |
| onClick={toggleMenu} | |
| aria-label={isExpanded ? "Close menu" : "Open menu"} | |
| aria-expanded={isExpanded} | |
| > | |
| <div | |
| className={cn( | |
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | |
| isHamburgerOpen && "translate-y-1 rotate-45" | |
| )} | |
| /> | |
| <div | |
| className={cn( | |
| "hamburger-line w-[30px] h-0.5 bg-foreground transition-[transform,opacity,margin] duration-300 ease-linear origin-[50%_50%] group-hover:opacity-75", | |
| isHamburgerOpen && "-translate-y-1 -rotate-45" | |
| )} | |
| /> | |
| </button> |
🤖 Prompt for AI Agents
In components/ui/card-nav.tsx around lines 179 to 202, replace the div acting as
a button with a native <button type="button">: move the className and onClick to
the button, remove role and tabIndex and the manual onKeyDown (native buttons
handle Enter/Space), and add aria-expanded={isHamburgerOpen} (use the actual
open state) and keep aria-label (update it if necessary). Also ensure any
styling or focus styles still apply to the button element and preserve the two
inner divs for the hamburger lines.
Summary by CodeRabbit
New Features
Style
Chores