-
Notifications
You must be signed in to change notification settings - Fork 1.1k
fix: dashboard flicker & improvements #1022
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
Conversation
|
Caution Review failedThe pull request is closed. WalkthroughReworks dashboard shell into a CSS grid, replaces the inline header with a new Top bar, adjusts desktop sidebar semantics/width, refines folder rename handlers, updates global scrollbar and grid rules, and adds an SVG Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant Router
participant Grid as dashboard-grid
participant Sidebar as Desktop Nav
participant Dashboard as DashboardInner
participant Top as Top Bar
participant Context as DashboardContext
participant Notifications
participant Theme
User->>Router: navigate to dashboard
Router->>Grid: render dashboard-grid
Grid->>Sidebar: mount Desktop Nav (motion.aside)
Grid->>Dashboard: mount DashboardInner (main area)
Dashboard->>Context: read activeOrganization/activeSpace
Dashboard->>Top: render Top with context
User->>Top: click bell
Top->>Notifications: toggle panel (click-away)
User->>Top: toggle theme
Top->>Theme: setTheme (use startViewTransition if available)
User->>Dashboard: rename folder
Dashboard->>Dashboard: prevent navigation, await rename update on Enter/blur
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (3)
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 |
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
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (2)
274-281: Don’t nest a textarea (interactive) inside a Link (interactive).This is invalid HTML/a11y and can cause focus/nav bugs. Refactor so the card isn’t an when renaming is possible; navigate via onClick/router instead.
Here’s a minimal direction (remove Link wrapper and handle navigation on the card):
- <Link - prefetch={false} - href={ - spaceId - ? `/dashboard/spaces/${spaceId}/folder/${id}` - : `/dashboard/folder/${id}` - } - > + {/* Use router-based navigation; avoids nesting interactive content inside <a> */} <div ref={folderRef} + role="link" + tabIndex={0} + onClick={() => { + if (isRenaming) return; + router.push( + spaceId ? `/dashboard/spaces/${spaceId}/folder/${id}` : `/dashboard/folder/${id}`, + ); + }} + onKeyDown={(e) => { + if (isRenaming) return; + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + router.push( + spaceId ? `/dashboard/spaces/${spaceId}/folder/${id}` : `/dashboard/folder/${id}`, + ); + } + }} ... - </div> -</Link> + </div>If keeping , ensure the renaming UI is outside the anchor element.
Also applies to: 340-361
263-265: Inconsistent spaceId handling may break moves outside a space.registerDropTarget uses a fallback spaceId; handleDrop doesn’t. Align both to avoid undefined spaceId.
Apply this diff:
- await moveVideoToFolder({ videoId: capData.id, folderId: id, spaceId }); + await moveVideoToFolder({ + videoId: capData.id, + folderId: id, + spaceId: spaceId ?? activeOrganization?.organization.id, + });apps/web/app/(org)/dashboard/_components/DashboardInner.tsx (1)
36-43: MembersDialog open state is unreachable — Top doesn't trigger itmembersDialogOpen is declared and passed to MembersDialog in apps/web/app/(org)/dashboard/_components/DashboardInner.tsx (useState at line ~15; render at lines ~36–41), but there are no other callers of setMembersDialogOpen in the repo (search shows only this file). MembersDialog is a controlled component (apps/web/app/(org)/dashboard/spaces/[spaceId]/components/MembersDialog.tsx). Action: either lift the open state into the parent that renders Top or pass a callback to Top and call it from Top (example: <Top onOpenMembers={() => setMembersDialogOpen(true)} />) so the dialog can actually be opened.
🧹 Nitpick comments (18)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsx (1)
21-23: Make the accessible name configurable (prop) and expose it via aria-label.Defaulting to “Box” may be misleading in some contexts. Let callers override the label and set it both on <title> and aria-label for robust a11y.
Apply this diff:
interface ReferIconProps extends MotionProps { size?: number; + label?: string; } const ReferIcon = forwardRef<ReferIconHandle, ReferIconProps>( - ({ size = 28, ...props }, ref) => { + ({ size = 28, label = "Refer", ...props }, ref) => { ... - <motion.svg + <motion.svg width={size} height={size} fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" + aria-label={label} variants={referVariants} animate={isAnimating ? "animate" : "normal"} onAnimationComplete={() => setIsAnimating(false)} {...props} > - <title>Box</title> + <title>{label}</title>Also applies to: 26-26, 44-55, 56-56
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (6)
55-55: Use a cross‑env timeout type.NodeJS.Timeout is incorrect in the browser. Use ReturnType.
- const animationTimerRef = useRef<NodeJS.Timeout | null>(null); + const animationTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
352-357: Prevent newline on Enter in textarea.Enter currently inserts a newline before exiting rename. Stop the default.
onKeyDown={async (e) => { if (e.key === "Enter") { + e.preventDefault(); setIsRenaming(false); if (updateName.trim() !== name) { await updateFolderNameHandler(); } } }}
181-187: Trim before saving and avoid redundant state toggles.You compare with trim() but save the untrimmed value; also setIsRenaming(false) is duplicated here and in updateFolderNameHandler.
- await updateFolder({ folderId: id, name: updateName }); + await updateFolder({ folderId: id, name: updateName.trim() });onBlur={async () => { - setIsRenaming(false); if (updateName.trim() !== name) { await updateFolderNameHandler(); } }}if (e.key === "Enter") { - e.preventDefault(); - setIsRenaming(false); + e.preventDefault(); if (updateName.trim() !== name) { await updateFolderNameHandler(); } }Also applies to: 345-350, 353-356
249-253: Comment contradicts behavior.Comment says keep open after drop but you play “folder-close”. Fix the comment or animation for clarity.
- // Keep the folder open after a successful drop + // Close the folder after a successful drop
49-52: Remove unused field in dragStateRef.isAnimating is never read.
const dragStateRef = useRef({ isDragging: false, - isAnimating: false, });
322-325: Minor a11y: cursor semantics while renaming.Consider toggling cursor-text on the renaming area when isRenaming to reflect edit mode.
apps/web/app/globals.css (2)
55-66: Dashboard grid: consider explicit row sizing.Two identical “main” rows without template rows may be redundant. A single row or explicit grid-template-rows can simplify layout.
.dashboard-grid { display: grid; grid-template-columns: auto 1fr 1fr; - grid-template-areas: - "sidebar main main" - "sidebar main main"; + grid-template-areas: "sidebar main main"; + /* Optionally: grid-template-rows: 1fr; */ height: 100dvh; width: 100vw; min-height: 100dvh; }
311-318: Mobile grid collapse: OK; minor simplification possible.Single-column template can be one row.
@media all and (max-width: 1024px) { .dashboard-grid { grid-template-columns: 1fr; - grid-template-areas: - "main" - "main"; + grid-template-areas: "main"; } }apps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsx (2)
45-46: Remove meaningless flex-1 on a grid item.flex-1 has no effect on a direct grid child; width is controlled by the animation.
- className={clsx( - "hidden relative z-50 flex-1 h-full [grid-area:sidebar] will-change-[width] lg:flex group bg-gray-1", - )} + className={clsx( + "hidden relative z-50 h-full [grid-area:sidebar] will-change-[width] lg:flex group bg-gray-1", + )}
68-81: Add an aria-label to the collapse toggle.Improves screen-reader discoverability.
<Button variant="white" onClick={toggleSidebarCollapsed} + aria-label="Toggle sidebar" className="absolute right-[-12px] size-7 p-0 min-w-[unset] top-[50%] transform -translate-y-1/2 rounded-full transition-colors z-10" >apps/web/app/(org)/dashboard/_components/DashboardInner.tsx (1)
23-24: grid-area on a non-grid child is a no-op.Only direct children of the grid container participate. Remove [grid-area:main] here to reduce confusion; the parent already sets it.
- className={ - "flex relative flex-col flex-1 h-full [grid-area:main] bg-gray-1" - } + className={"flex relative flex-col flex-1 h-full bg-gray-1"}apps/web/app/(org)/dashboard/layout.tsx (1)
82-90: Place MobileTab inside the main grid area (or render it with [grid-area:main])layout.tsx currently renders as an unplaced sibling of the [grid-area:main] container (apps/web/app/(org)/dashboard/layout.tsx) — that will create implicit grid rows. MobileTab (apps/web/app/(org)/dashboard/_components/MobileTab.tsx) does not accept/forward className, so adding a prop to the component won't propagate. Fix by moving the component into the main area or by updating/wrapping MobileTab so it renders with [grid-area:main].
- <DashboardInner>{children}</DashboardInner> - </div> - <MobileTab /> + <DashboardInner>{children}</DashboardInner> + <MobileTab /> + </div>apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx (5)
144-160: Make the theme toggle a button for keyboard/a11y.Current clickable div isn’t keyboard-focusable; use a button with an accessible label.
- <div - onClick={() => { - if (document.startViewTransition) { - document.startViewTransition(() => { - setThemeHandler(theme === "light" ? "dark" : "light"); - }); - } else { - setThemeHandler(theme === "light" ? "dark" : "light"); - } - }} - className="hidden justify-center items-center rounded-full transition-colors cursor-pointer bg-gray-3 lg:flex hover:bg-gray-5 size-9" - > + <button + type="button" + aria-label={theme === "light" ? "Switch to dark theme" : "Switch to light theme"} + onClick={() => { + const startViewTransition = + (document as any).startViewTransition?.bind(document); + if (typeof startViewTransition === "function") { + startViewTransition(() => + setThemeHandler(theme === "light" ? "dark" : "light"), + ); + } else { + setThemeHandler(theme === "light" ? "dark" : "light"); + } + }} + className="hidden justify-center items-center rounded-full transition-colors bg-gray-3 lg:flex hover:bg-gray-5 size-9" + > <FontAwesomeIcon className="text-gray-12 size-3.5 view-transition-theme-icon" icon={theme === "dark" ? faMoon : faSun} /> - </div> + </button>
240-270: Use a real button for the user menu trigger.Improves semantics, focus handling, and ARIA without custom key handlers.
- <PopoverTrigger asChild> - <div - data-state={menuOpen ? "open" : "closed"} - className="flex gap-2 justify-between items-center p-2 rounded-xl border data-[state=open]:border-gray-3 data-[state=open]:bg-gray-3 border-transparent transition-colors cursor-pointer group lg:gap-6 hover:border-gray-3" - > + <PopoverTrigger asChild> + <button + type="button" + aria-haspopup="menu" + aria-expanded={menuOpen} + data-state={menuOpen ? "open" : "closed"} + className="flex gap-2 justify-between items-center p-2 rounded-xl border data-[state=open]:border-gray-3 data-[state=open]:bg-gray-3 border-transparent transition-colors group lg:gap-6 hover:border-gray-3" + > ... - </div> + </button> </PopoverTrigger>
338-338: Add accessible label to Refer link (icon-only).Screen readers need a descriptive label.
- <Link href="/dashboard/refer" className="hidden relative lg:block"> + <Link + href="/dashboard/refer" + aria-label="Refer friends and earn 40% commission" + className="hidden relative lg:block" + >
172-231: Avoid staleuseMemodependencies for menu items.
isSubscribedchanges won’t update the menu. Add it to deps.- const menuItems = useMemo( + const menuItems = useMemo( () => [ ... ], - [], + [isSubscribed], );
1-1: Rename file to kebab-case per guidelines.
Top.tsx→top.tsx(and update imports).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (7)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsx(1 hunks)apps/web/app/(org)/dashboard/_components/DashboardInner.tsx(1 hunks)apps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsx(2 hunks)apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx(1 hunks)apps/web/app/(org)/dashboard/caps/components/Folder.tsx(3 hunks)apps/web/app/(org)/dashboard/layout.tsx(1 hunks)apps/web/app/globals.css(3 hunks)
🧰 Additional context used
📓 Path-based instructions (4)
apps/web/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
apps/web/**/*.{ts,tsx}: Use TanStack Query v5 for client-side server state and data fetching in the web app
Mutations should call Server Actions and perform precise cache updates with setQueryData/setQueriesData, avoiding broad invalidations
Prefer Server Components for initial data and pass initialData to client components for React Query hydration
Files:
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsxapps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsxapps/web/app/(org)/dashboard/_components/Navbar/Top.tsxapps/web/app/(org)/dashboard/_components/DashboardInner.tsxapps/web/app/(org)/dashboard/layout.tsxapps/web/app/(org)/dashboard/caps/components/Folder.tsx
{apps/web,packages/ui}/**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
{apps/web,packages/ui}/**/*.{ts,tsx}: Use Tailwind CSS exclusively for styling in the web app and shared React UI components
Component naming: React components in PascalCase; hooks in camelCase starting with 'use'
Files:
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsxapps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsxapps/web/app/(org)/dashboard/_components/Navbar/Top.tsxapps/web/app/(org)/dashboard/_components/DashboardInner.tsxapps/web/app/(org)/dashboard/layout.tsxapps/web/app/(org)/dashboard/caps/components/Folder.tsx
**/*.{ts,tsx}
📄 CodeRabbit inference engine (CLAUDE.md)
Use strict TypeScript and avoid any; prefer shared types from packages
**/*.{ts,tsx}: Use Biome to format/lint TypeScript with a 2-space indent
TypeScript file names should be kebab-case (e.g., user-menu.tsx)
Files:
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsxapps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsxapps/web/app/(org)/dashboard/_components/Navbar/Top.tsxapps/web/app/(org)/dashboard/_components/DashboardInner.tsxapps/web/app/(org)/dashboard/layout.tsxapps/web/app/(org)/dashboard/caps/components/Folder.tsx
**/*.{tsx,jsx}
📄 CodeRabbit inference engine (AGENTS.md)
React/Solid components should be named using PascalCase
Files:
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsxapps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsxapps/web/app/(org)/dashboard/_components/Navbar/Top.tsxapps/web/app/(org)/dashboard/_components/DashboardInner.tsxapps/web/app/(org)/dashboard/layout.tsxapps/web/app/(org)/dashboard/caps/components/Folder.tsx
🧬 Code graph analysis (3)
apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx (4)
apps/web/app/(org)/dashboard/Contexts.tsx (2)
useDashboardContext(42-42)useTheme(40-40)apps/web/components/UpgradeModal.tsx (1)
UpgradeModal(59-305)apps/web/app/(org)/dashboard/_components/AnimatedIcons/Download.tsx (1)
DownloadIconHandle(9-12)apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsx (1)
ReferIconHandle(16-19)
apps/web/app/(org)/dashboard/_components/DashboardInner.tsx (1)
apps/web/app/(org)/dashboard/Contexts.tsx (1)
useDashboardContext(42-42)
apps/web/app/(org)/dashboard/layout.tsx (1)
apps/web/app/(org)/dashboard/_components/Navbar/Desktop.tsx (1)
DesktopNav(13-85)
⏰ 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). (3)
- GitHub Check: Build Desktop (x86_64-pc-windows-msvc, windows-latest)
- GitHub Check: Build Desktop (aarch64-apple-darwin, macos-latest)
- GitHub Check: Analyze (rust)
🔇 Additional comments (5)
apps/web/app/(org)/dashboard/_components/AnimatedIcons/Refer.tsx (1)
56-56: A11y label addition looks good.Adding an SVG <title> improves accessible naming.
apps/web/app/globals.css (1)
82-85: Scrollbar tweaks LGTM.Thumb colors + radius for light/dark are reasonable defaults.
Also applies to: 86-90
apps/web/app/(org)/dashboard/_components/DashboardInner.tsx (1)
20-21: Header integration LGTM.Delegating header UI to Top simplifies this component.
apps/web/app/(org)/dashboard/caps/components/Folder.tsx (1)
1-403: Do not rename this file in isolation — repo uses PascalCase filenames.Multiple TS/TSX files under apps/web (including apps/web/app/(org)/dashboard/caps/components/Folder.tsx) use PascalCase; renaming only this file to kebab-case would be inconsistent. Either perform a repo-wide kebab-case migration with lint/CI, or keep the existing PascalCase convention.
Likely an incorrect or invalid review comment.
apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx (1)
141-142: VerifyNotificationsforwards its ref.
useClickAwayexpects the ref to be attached to the root element insideNotifications. If it doesn’t forward a ref to a focusable/container element, click-away may misfire.
| const notificationsRef: MutableRefObject<HTMLDivElement> = useClickAway( | ||
| (e) => { | ||
| if (bellRef.current && !bellRef.current.contains(e.target as Node)) { | ||
| setToggleNotifications(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.
TS strictness: nullability and experimental API types will fail builds.
useClickAwayref must allownull(and can use its generic).document.startViewTransitionneeds a safe/typed access.useRef<...>(null)must include| null.
- const notificationsRef: MutableRefObject<HTMLDivElement> = useClickAway(
+ const notificationsRef = useClickAway<HTMLDivElement>(
(e) => {
if (bellRef.current && !bellRef.current.contains(e.target as Node)) {
setToggleNotifications(false);
}
},
);
- if (document.startViewTransition) {
- document.startViewTransition(() => {
- setThemeHandler(theme === "light" ? "dark" : "light");
- });
- } else {
- setThemeHandler(theme === "light" ? "dark" : "light");
- }
+ const startViewTransition =
+ (document as any).startViewTransition?.bind(document);
+ if (typeof startViewTransition === "function") {
+ startViewTransition(() => {
+ setThemeHandler(theme === "light" ? "dark" : "light");
+ });
+ } else {
+ setThemeHandler(theme === "light" ? "dark" : "light");
+ }
- const iconRef = useRef<DownloadIconHandle>(null);
+ const iconRef = useRef<DownloadIconHandle | null>(null);
- const iconRef = useRef<ReferIconHandle>(null);
+ const iconRef = useRef<ReferIconHandle | null>(null);Also applies to: 146-153, 303-303, 335-335
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx around lines 66-72
(and similarly at 146-153, 303, 335), the refs and experimental API uses are not
typed for nullability or safe access: change the useClickAway ref to use the
generic and allow null (e.g., MutableRefObject<HTMLDivElement | null> by using
useClickAway<HTMLDivElement | null>(...)), update every useRef<T>(null) to
useRef<T | null>(null), and guard/cast accesses to document.startViewTransition
with a runtime/type-safe check (e.g., check typeof (document as
any).startViewTransition === "function" or narrow document with an interface
that has optional startViewTransition before calling) so TypeScript strict mode
and experimental API typings do not fail builds.
| </div> | ||
| </div> | ||
| <div className="flex gap-4 items-center"> | ||
| {buildEnv.NEXT_PUBLIC_IS_CAP && <ReferButton />} |
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 env flag checks (string vs boolean) to avoid incorrect UI gating.
buildEnv.NEXT_PUBLIC_IS_CAP is a string (e.g., "true"/"false"). Current truthy checks will render CAP-only UI even when set to "false".
- {buildEnv.NEXT_PUBLIC_IS_CAP && <ReferButton />}
+ {buildEnv.NEXT_PUBLIC_IS_CAP === "true" && <ReferButton />}
- showCondition: !isSubscribed && buildEnv.NEXT_PUBLIC_IS_CAP,
+ showCondition: !isSubscribed && buildEnv.NEXT_PUBLIC_IS_CAP === "true",
- showCondition: buildEnv.NEXT_PUBLIC_IS_CAP,
+ showCondition: buildEnv.NEXT_PUBLIC_IS_CAP === "true",Also applies to: 190-191, 198-199
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx around lines 106,
190-191, and 198-199, the code currently uses the string env variable
buildEnv.NEXT_PUBLIC_IS_CAP in a truthy check which will render CAP-only UI when
the variable is the string "false"; change each conditional to explicitly check
the string value (for example normalize and compare:
String(buildEnv.NEXT_PUBLIC_IS_CAP).toLowerCase() === 'true' or
buildEnv.NEXT_PUBLIC_IS_CAP === 'true') so the UI is gated only when the env
flag is actually set to "true".
| const MenuItem = memo(({ icon, name, href, onClick, iconClassName }: Props) => { | ||
| const iconRef = useRef<DownloadIconHandle>(null); | ||
| return ( | ||
| <CommandItem | ||
| key={name} | ||
| className="px-2 py-1.5 rounded-lg transition-colors duration-300 cursor-pointer hover:bg-gray-5 group" | ||
| onSelect={onClick} | ||
| onMouseEnter={() => { | ||
| iconRef.current?.startAnimation(); | ||
| }} | ||
| onMouseLeave={() => { | ||
| iconRef.current?.stopAnimation(); | ||
| }} | ||
| > | ||
| <Link | ||
| className="flex gap-2 items-center w-full" | ||
| href={href ?? "#"} | ||
| onClick={onClick} | ||
| > | ||
| <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5"> | ||
| {cloneElement(icon, { | ||
| ref: iconRef, | ||
| className: iconClassName, | ||
| size: 14, | ||
| })} | ||
| </div> | ||
| <p className={clsx("text-sm text-gray-12")}>{name}</p> | ||
| </Link> | ||
| </CommandItem> | ||
| ); |
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.
Prevent double-invocation on menu item clicks; avoid nested interactive conflicts.
CommandItem's onSelect and the inner Link’s onClick both call handlers, causing duplicate actions (e.g., double signOut, modal opens twice).
- <CommandItem
- key={name}
- className="px-2 py-1.5 rounded-lg transition-colors duration-300 cursor-pointer hover:bg-gray-5 group"
- onSelect={onClick}
- onMouseEnter={() => {
- iconRef.current?.startAnimation();
- }}
- onMouseLeave={() => {
- iconRef.current?.stopAnimation();
- }}
- >
- <Link
- className="flex gap-2 items-center w-full"
- href={href ?? "#"}
- onClick={onClick}
- >
+ <CommandItem
+ key={name}
+ className="px-2 py-1.5 rounded-lg transition-colors duration-300 cursor-pointer hover:bg-gray-5 group"
+ onMouseEnter={() => {
+ iconRef.current?.startAnimation();
+ }}
+ onMouseLeave={() => {
+ iconRef.current?.stopAnimation();
+ }}
+ >
+ {href ? (
+ <Link className="flex gap-2 items-center w-full" href={href} onClick={onClick}>
+ <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5">
+ {cloneElement(icon, { ref: iconRef, className: iconClassName, size: 14 })}
+ </div>
+ <p className={clsx("text-sm text-gray-12")}>{name}</p>
+ </Link>
+ ) : (
+ <button
+ type="button"
+ className="flex gap-2 items-center w-full text-left"
+ onClick={onClick}
+ >
+ <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5">
+ {cloneElement(icon, { ref: iconRef, className: iconClassName, size: 14 })}
+ </div>
+ <p className={clsx("text-sm text-gray-12")}>{name}</p>
+ </button>
+ )}
- <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5">
- {cloneElement(icon, {
- ref: iconRef,
- className: iconClassName,
- size: 14,
- })}
- </div>
- <p className={clsx("text-sm text-gray-12")}>{name}</p>
- </Link>
</CommandItem>📝 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 MenuItem = memo(({ icon, name, href, onClick, iconClassName }: Props) => { | |
| const iconRef = useRef<DownloadIconHandle>(null); | |
| return ( | |
| <CommandItem | |
| key={name} | |
| className="px-2 py-1.5 rounded-lg transition-colors duration-300 cursor-pointer hover:bg-gray-5 group" | |
| onSelect={onClick} | |
| onMouseEnter={() => { | |
| iconRef.current?.startAnimation(); | |
| }} | |
| onMouseLeave={() => { | |
| iconRef.current?.stopAnimation(); | |
| }} | |
| > | |
| <Link | |
| className="flex gap-2 items-center w-full" | |
| href={href ?? "#"} | |
| onClick={onClick} | |
| > | |
| <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5"> | |
| {cloneElement(icon, { | |
| ref: iconRef, | |
| className: iconClassName, | |
| size: 14, | |
| })} | |
| </div> | |
| <p className={clsx("text-sm text-gray-12")}>{name}</p> | |
| </Link> | |
| </CommandItem> | |
| ); | |
| const MenuItem = memo(({ icon, name, href, onClick, iconClassName }: Props) => { | |
| const iconRef = useRef<DownloadIconHandle>(null); | |
| return ( | |
| <CommandItem | |
| key={name} | |
| className="px-2 py-1.5 rounded-lg transition-colors duration-300 cursor-pointer hover:bg-gray-5 group" | |
| onMouseEnter={() => { | |
| iconRef.current?.startAnimation(); | |
| }} | |
| onMouseLeave={() => { | |
| iconRef.current?.stopAnimation(); | |
| }} | |
| > | |
| {href ? ( | |
| <Link className="flex gap-2 items-center w-full" href={href} onClick={onClick}> | |
| <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5"> | |
| {cloneElement(icon, { ref: iconRef, className: iconClassName, size: 14 })} | |
| </div> | |
| <p className={clsx("text-sm text-gray-12")}>{name}</p> | |
| </Link> | |
| ) : ( | |
| <button | |
| type="button" | |
| className="flex gap-2 items-center w-full text-left" | |
| onClick={onClick} | |
| > | |
| <div className="flex-shrink-0 flex items-center justify-center w-3.5 h-3.5"> | |
| {cloneElement(icon, { ref: iconRef, className: iconClassName, size: 14 })} | |
| </div> | |
| <p className={clsx("text-sm text-gray-12")}>{name}</p> | |
| </button> | |
| )} | |
| </CommandItem> | |
| ); |
🤖 Prompt for AI Agents
In apps/web/app/(org)/dashboard/_components/Navbar/Top.tsx around lines 302-331,
the MenuItem currently passes the same handler to CommandItem.onSelect and the
inner Link.onClick causing duplicate invocations; fix by removing the onClick
from the inner Link (leave href for navigation and keep Link as non-interactive
wrapper) and centralize the action in CommandItem.onSelect (move any navigation
or action logic into the onSelect handler so keyboard/mouse both trigger exactly
once), ensuring no nested interactive handlers remain.
This PR:
Summary by CodeRabbit