Skip to content

sidebar#118

Merged
Friedrich482 merged 12 commits intomainfrom
features
Feb 18, 2026
Merged

sidebar#118
Friedrich482 merged 12 commits intomainfrom
features

Conversation

@Friedrich482
Copy link
Owner

@Friedrich482 Friedrich482 commented Feb 18, 2026

Commits

  • feat: sidebar

    • added the shadcn sidebar component
    • integrated it with the layout of the current dashboard
    • extracted a separated layout for auth pages
    • updated components on the dashboard and project pages to make everything responsive accounting the addition of the sidebar
    • removed the main-layout class from the index.css and updated the main layout.tsx component by removing the useEffect using that class
  • feat: reset scroll on page navigation

    • added the navigation-reset-wrapper.tsx component that brings the user back to the top of a page when navigating
    • added the title in the dependencies array of the useEffect in the use-page-title hook
  • fix: TooltipProvider

    • added only one root tooltip provider in the App.tsx component
  • fix: circle packing chart

    • fixed the responsiveness issues in the circle-packing chart
    • reset the height and width of the svg wrapper when the group check box is toggled to bring the bubbles in the center
  • fix: page reset

    • added a useEffect to reset the page of files when a language is selected in the LanguagesDropDown component or when the period/custom range changes
  • feat: link + query params

    • added a new component called link-with-query that wraps the react-router link and adds the search params to the path navigated to
  • updated affected components : app-sidebar, auth-dropdown (removed the AUTH_DROPDOWN_ITEMS constant), navbar, title, period-projects

  • fix: token conflict

    • fixed the naming conflict between the google login token and the password reset token by renaming the reset-password token to reset-password-token instead of just token. This conflict lead to the page reset-password page to be stuck in an infinite loop trying to navigate to the dashboard when the reset password procedure is over
    • increased the rate limits to 600 requests for 15 minutes
  • fix: emails year

    • used the real year instead of hardcoding 2025 in the emails body (get-onboarding-email-body and get-password-reset-email-body)
    • prettified the error thrown by the redirectToVSCodeAfterGoogleAuthLoader when the zod validation fails
  • feat: new langages subset

    • added csv subsets to the langages subsets: csv (semicolon), csv (pipe), csv (whitespace) and dynamic csv
    • bumped the extension version to v0.0.51

Summary by CodeRabbit

  • New Features

    • New sidebar navigation with project listing, responsive mobile support, keyboard toggle and preserved query params when navigating.
    • New UI primitives: Sheet, Collapsible, Separator; layout wrapper that resets scroll on navigation.
  • Improvements

    • Email templates now use the current year in footers.
    • Increased API rate limit for broader usage.
    • Dashboard layout and responsive styling refinements.
  • Updates

    • Added additional CSV language variants.
    • Password reset flow uses a renamed token query parameter.

- added the shadcn sidebar component
- integrated it with the layout of the current dashboard
- extracted a separated layout for auth pages
- updated components on the dashboard and project pages to make everything responsive accounting the addition of the sidebar
- removed the `main-layout` class from the `index.css` and updated the main layout.tsx  component by removing the useEffect using that class
- added the `navigation-reset-wrapper.tsx` component that brings the user back to the top of a page when navigating
- added the title in the dependencies array of the useEffect in the `use-page-title` hook
- added only one root tooltip provider in the App.tsx component
- fixed the responsiveness issues in the circle-packing chart
- reset the height and width of the svg wrapper when the group check box is toggled to bring the bubbles in the center
- added a useEffect to reset the page of files when a language is selected in the LanguagesDropDown component or when the period/custom range changes
- added a new component called `link-with-query` that wraps the react-router link and adds the search params to the path navigated to
- updated affected components : `app-sidebar`, `auth-dropdown` (removed the `AUTH_DROPDOWN_ITEMS` constant), `navbar`, `title`, `period-projects`
- fixed the naming conflict between the google login token and the password reset token by renaming the reset-password token to `reset-password-token` instead of just `token`. This conflict lead to the page reset-password page to be stuck in an infinite loop trying to navigate to the dashboard when the reset password procedure is over
- increased the rate limits to 600 requests for 15 minutes
- used the real year instead of hardcoding 2025 in the emails body (`get-onboarding-email-body` and `get-password-reset-email-body`)
- prettified the error thrown by the `redirectToVSCodeAfterGoogleAuthLoader` when the zod validation fails
- added csv subsets to the langages subsets: csv (semicolon), csv (pipe), csv (whitespace) and dynamic csv
- bumped the extension version to `v0.051`
@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

📝 Walkthrough

Walkthrough

Adds a shared Sidebar system and UI primitives (Sheet, Separator, Collapsible), restructures dashboard layout/providers (AppSidebar, SidebarProvider, Wrapper), preserves query strings via LinkWithQuery, injects dynamic current year into email templates, increases TRPC default rate limit, and applies multiple responsive/UI and minor loader/auth adjustments.

Changes

Cohort / File(s) Summary
Shared UI primitives
packages/ui/src/components/ui/sidebar.tsx, packages/ui/src/components/ui/sheet.tsx, packages/ui/src/components/ui/separator.tsx, packages/ui/src/components/ui/collapsible.tsx
Added a full-featured Sidebar system (context/provider, many subcomponents, keyboard shortcuts, persistence, mobile sheet) plus new Sheet, Separator, and Collapsible wrappers. Review public APIs, client-only concerns, and wide surface area.
UI globals & hooks
packages/ui/src/globals.css, packages/ui/src/hooks/use-mobile.tsx
Added theme shadow/sidebar tokens and a new useIsMobile hook for breakpoint detection; verify theme consistency and usage.
Dashboard layout & sidebar
apps/dashboard/src/App.tsx, apps/dashboard/src/components/layout/layout.tsx, apps/dashboard/src/components/layout/app-sidebar.tsx, apps/dashboard/src/components/layout/navigation-reset-wrapper.tsx
Integrated AppSidebar and SidebarProvider, added Wrapper to reset scroll on route change, restructured layout to include sidebar and adjust header/footer placement; AppSidebar contains TRPC queries, pagination, and active-link logic.
Routing & navigation helpers
apps/dashboard/src/components/common/link-with-query.tsx, apps/dashboard/src/main.tsx
Added LinkWithQuery (preserves current search params) and an AuthLayout route wrapper for auth pages.
Header & header subcomponents
apps/dashboard/src/components/layout/header/... (title.tsx,navbar.tsx,auth-dropdown.tsx,gravatar-avatar.tsx,header.tsx)
Replaced BurgerMenu/Logo with SidebarTrigger/Separator, switched Link → LinkWithQuery, made Gravatar accept className, and adjusted header positioning to sticky.
Removed header components
apps/dashboard/src/components/layout/header/burger-menu.tsx, apps/dashboard/src/components/layout/header/sidebar.tsx
Deleted legacy BurgerMenu and SideBar components and their exports.
Files & charts adjustments
apps/dashboard/src/features/files-list/..., apps/dashboard/src/features/files-circle-packing-chart/..., apps/dashboard/src/features/*chart*/...
Refactored files list (memoization, grid layout, period-store handling), threaded resetSVGDimensions into circle-packing options, removed some TooltipProvider usages, and applied multiple responsive/tailwind tweaks.
Email templates & auth router
apps/api/src/email/utils/get-onboarding-email-body.ts, apps/api/src/email/utils/get-password-reset-email-body.ts, apps/api/src/auth/auth.router.ts
Converted email template arrow expressions to block-bodied functions and inject currentYear into footers; tiny punctuation formatting change in AuthRouter constructor only.
TRPC service
apps/api/src/trpc/trpc.service.ts
Default rateLimiter max increased from 300 → 600; usage switched to nullish coalescing when params are absent.
Loaders & routing params
apps/dashboard/src/loaders/auth-loader.ts, apps/dashboard/src/loaders/password-reset-loader.ts, apps/dashboard/src/hooks/use-page-title.tsx
Centralized Zod error formatting; password-reset loader now reads reset-password-token; usePageTitle effect depends on [title].
Misc dashboard styling & small UX
multiple files under apps/dashboard/src/...
Many responsive Tailwind adjustments (padding, wrap-anywhere, chevron visibility), removed root .main-layout CSS rule, and small markup/formatting edits.
VSCode extension
apps/vscode-extension/package.json, apps/vscode-extension/src/constants.ts
Version bump 0.0.50 → 0.0.51; added additional CSV mappings to SUBSET_LANGUAGES.
Large diffs to inspect closely
packages/ui/src/components/ui/sidebar.tsx, apps/dashboard/src/components/layout/app-sidebar.tsx, packages/ui/src/components/ui/sheet.tsx
Extensive new logic/state and exported components — prioritize accessibility, keyboard handling, cookie persistence, SSR/client boundaries, typing, and interactions with TRPC and routing.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    actor User
    participant App as AppSidebar
    participant TRPC as TRPC Client
    participant API as Backend API
    participant Router as React Router

    User->>App: open dashboard / change period / paginate
    App->>TRPC: query getPeriodProjects(period, range?, page)
    TRPC->>API: fetch projects (period, start, end, page)
    API-->>TRPC: projects + hasMore
    TRPC-->>App: return data
    App->>App: merge projects, update pagination state
    User->>App: click project link
    App->>Router: navigate via LinkWithQuery (to + current search)
    Router-->>User: route change (search params preserved)
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

🐇 I hopped through code with whiskers bright,
A sidebar sprung to guide the sight,
Links keep queries, charts stretch wide,
Emails show the year with pride,
Hooray — the dashboard dances tonight! 🎉

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'sidebar' accurately describes the main change in the PR: adding and integrating a shadcn sidebar component with the dashboard layout.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch features

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

Caution

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

⚠️ Outside diff range comments (1)
apps/dashboard/src/features/files-list/components/languages-dropdown.tsx (1)

29-35: ⚠️ Potential issue | 🟡 Minor

Icon-only button at narrow viewports lacks an accessible name.

With max-[20rem]:hidden, Tailwind sets display: none on the "Languages" span, removing it from the accessibility tree. At viewports ≤ 20rem the <Button> has no visible text and no aria-label, making it unnamed for assistive technologies (violates WCAG 2.1 SC 4.1.2).

♿ Proposed fix: add `aria-label` to the button
        <Button
          variant="secondary"
          className="flex items-center justify-center gap-2"
+         aria-label="Languages"
        >
          <span className="max-[20rem]:hidden">Languages</span>
          <ChevronDown />
        </Button>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/features/files-list/components/languages-dropdown.tsx`
around lines 29 - 35, The button rendered in the Languages dropdown (the
<Button> wrapping the "Languages" <span> and <ChevronDown />) becomes icon-only
at narrow viewports because the "Languages" span is hidden; add an accessible
name by setting an aria-label (e.g., aria-label="Languages") on that <Button>
element so assistive tech still perceives the control when the span is hidden,
ensuring the label matches the visible text and remains consistent with the
<ChevronDown /> icon.
🧹 Nitpick comments (12)
apps/api/src/trpc/trpc.service.ts (1)

53-53: Consider monitoring for increased abuse risk after doubling the global default.

Raising max from 300 → 600 per 15-minute window doubles the allowed burst for every endpoint that doesn't supply an explicit rate-limiter key/max pair. Since both publicProcedure and protectedProcedure fall through to this default, the change broadens the attack surface at the same IP granularity. If you don't already have alerting on TOO_MANY_REQUESTS error rates, now is a good time to add it so you can detect abuse patterns before they saturate downstream resources.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/api/src/trpc/trpc.service.ts` at line 53, The default rate limit max was
doubled (rateLimiterParams -> max = 600) expanding burst allowances for
publicProcedure and protectedProcedure; add observability and safer defaults:
revert the implicit default back to the previous conservative value (e.g., 300)
in trpc.service.ts or require explicit overrides, and instrument the rate-limit
rejection handler (where TOO_MANY_REQUESTS is raised) to emit a metric/alert
(counter/timestamp with labels like endpoint/procedure/IP) so ops can create an
alert on spikes; update publicProcedure/protectedProcedure callers to pass
explicit key/max if they need higher limits.
apps/dashboard/src/loaders/auth-loader.ts (1)

102-109: googleAuthLoader still uses the old manual error aggregation — inconsistent with the PR's centralisation effort

redirectToVSCodeAfterGoogleAuthLoader was updated to use formatZodError, but googleAuthLoader still concatenates issue messages manually. Note that the error thrown here is swallowed by the enclosing try-catch (line 115), so the formatting difference has no user-visible impact today — but it's an inconsistency worth cleaning up.

♻️ Proposed fix
       if (!validatedCallBackUrl.success) {
-        throw new Error(
-          validatedCallBackUrl.error.issues.reduce(
-            (acc, curr) => acc + curr.message,
-            "",
-          ),
-        );
+        throw new Error(formatZodError(validatedCallBackUrl.error));
       }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/loaders/auth-loader.ts` around lines 102 - 109, Replace
the manual concatenation of Zod issue messages in googleAuthLoader with the
centralized formatter: when validatedCallBackUrl.success is false, throw new
Error(formatZodError(validatedCallBackUrl.error)) instead of using
issues.reduce(...); ensure the function uses the existing formatZodError helper
(import or reference the same symbol used by
redirectToVSCodeAfterGoogleAuthLoader) so both loaders share consistent Zod
error formatting (note the thrown error is caught by the surrounding try/catch
in googleAuthLoader).
apps/dashboard/src/features/files-circle-packing-chart/hooks/use-animate-chart.tsx (1)

42-44: useMemo is unnecessary overhead for trivially cheap arithmetic.

height * 1e-9 and width * 1e-9 are single multiplications — the useMemo bookkeeping (dep tracking, cache allocation, cache invalidation check) costs more than the computation itself. Plain const variables are equivalent and simpler.

♻️ Proposed refactor
-  const boundaryHeightMargin = useMemo(() => height * 1e-9, [height]);
-
-  const boundaryWidthMargin = useMemo(() => width * 1e-9, [width]);
+  const boundaryHeightMargin = height * 1e-9;
+  const boundaryWidthMargin = width * 1e-9;

If useMemo is removed from these two, useMemo can be dropped from the import at line 1 if it has no other usages in this file.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/features/files-circle-packing-chart/hooks/use-animate-chart.tsx`
around lines 42 - 44, Remove the unnecessary useMemo wrappers around the trivial
multiplications: replace the computed values derived in boundaryHeightMargin and
boundaryWidthMargin (currently using useMemo with height * 1e-9 and width *
1e-9) with plain const assignments, and then remove useMemo from the import list
if it has no other usages in the file; update references to boundaryHeightMargin
and boundaryWidthMargin as needed.
apps/dashboard/src/features/auth/components/layout.tsx (1)

3-8: Consider renaming the component AuthLayout to match its aliased import.

This file and apps/dashboard/src/components/layout/layout.tsx both export a symbol named Layout. Because the names collide, main.tsx must alias it on import (Layout as AuthLayout). Renaming the export here eliminates that alias and makes the intent self-documenting.

♻️ Proposed rename
-export const Layout = () => {
+export const AuthLayout = () => {
   return (
     <div className="flex-1">
       <Outlet />
     </div>
   );
 };

And in apps/dashboard/src/main.tsx:

-import { Layout as AuthLayout } from "@/features/auth/components/layout";
+import { AuthLayout } from "@/features/auth/components/layout";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/features/auth/components/layout.tsx` around lines 3 - 8,
Rename the exported React component from Layout to AuthLayout (change the symbol
exported by the component currently declared as Layout to AuthLayout) and update
all imports/usages that currently import it (notably the place where it was
imported as Layout as AuthLayout) to import AuthLayout directly; specifically,
update the component declaration name (Layout -> AuthLayout), its export, and
any references/imports in main.tsx and other consumers so there is no aliased
import.
packages/ui/src/components/ui/sidebar.tsx (1)

80-85: Consider adding SameSite and Secure attributes to the sidebar state cookie.

The cookie is set without SameSite or Secure flags. While this is just a UI preference cookie with no security implications, browsers default SameSite to Lax which is fine, but being explicit is a good practice. If the dashboard is served over HTTPS, Secure ensures the cookie isn't sent over plain HTTP.

Optional fix
-      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}`;
+      document.cookie = `${SIDEBAR_COOKIE_NAME}=${openState}; path=/; max-age=${SIDEBAR_COOKIE_MAX_AGE}; SameSite=Lax; Secure`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/ui/src/components/ui/sidebar.tsx` around lines 80 - 85, The cookie
assignment for SIDEBAR_COOKIE_NAME in the effect using setOpenProp/open should
explicitly include SameSite and Secure attributes: append "; SameSite=Lax" (or
your chosen policy) to the cookie string and add "; Secure" when the app is
served over HTTPS (detect via location.protocol === 'https:' or
window.isSecureContext) so the code that sets document.cookie with
SIDEBAR_COOKIE_NAME and SIDEBAR_COOKIE_MAX_AGE includes these flags.
apps/dashboard/src/components/layout/navigation-reset-wrapper.tsx (1)

4-12: Consider a more descriptive export name than Wrapper.

The file is named navigation-reset-wrapper.tsx but exports Wrapper, which is very generic. At the import site (App.tsx), Wrapper doesn't convey what it does. A name like ScrollResetWrapper or NavigationScrollReset would be self-documenting.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/navigation-reset-wrapper.tsx` around
lines 4 - 12, Rename the generic exported component Wrapper to a descriptive
name (e.g., ScrollResetWrapper or NavigationScrollReset) to reflect its
behavior; update the component declaration/export (Wrapper) to the new name and
change all import sites (for example where App.tsx imports Wrapper) to the new
identifier so references remain consistent; keep the existing implementation
(useLocation, useLayoutEffect, window.scrollTo) unchanged—only change the
component name and its imports/exports to improve clarity.
apps/dashboard/src/App.tsx (1)

99-105: SidebarProvider registers a keyboard shortcut listener (Ctrl/Cmd+B) globally, including on auth pages where it's unused.

The route structure confirms auth pages (login, register, forgot-password, etc.) render through a separate <AuthLayout />, but since SidebarProvider wraps the <Outlet /> in App.tsx, the keyboard listener and cookie persistence are active on all routes—including auth pages that don't render a sidebar.

While auth pages won't break (the listener simply has no sidebar to toggle), this is unnecessary overhead. Consider moving SidebarProvider to wrap only the dashboard layout, or conditionally apply it based on the route.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/App.tsx` around lines 99 - 105, The SidebarProvider is
mounted globally in App.tsx causing its keyboard listener and cookie persistence
to run on auth routes; move the SidebarProvider so it only wraps the dashboard
routes (e.g., wrap it around the dashboard layout/component that renders the
sidebar, such as DashboardLayout) or conditionally render SidebarProvider in
App.tsx by checking the current route (exclude paths like login, register,
forgot-password) before mounting it; adjust references in App.tsx (remove
SidebarProvider around Outlet/Wrapper) and ensure the sidebar-related
state/hooks still wrap the components that need them (Wrapper, components inside
DashboardLayout) so the global listener is not active on AuthLayout routes.
apps/dashboard/src/components/layout/app-sidebar.tsx (3)

96-99: The header <div> with cursor-pointer isn't interactive — consider making it a link or button.

The SidebarMenuButton with asChild delegates rendering to its child, which is a plain <div>. A <div> with cursor-pointer is not keyboard-accessible (no focus/activation). If this is meant to be clickable (e.g., navigate to a user profile or dashboard root), wrap it in a <LinkWithQuery> or <button>. If it's purely decorative, remove cursor-pointer.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/app-sidebar.tsx` around lines 96 - 99,
The container div that currently renders the Logo and {username} is marked
cursor-pointer but is not keyboard-accessible because the SidebarMenuButton
(with asChild) delegates to a plain <div>; change the child to an interactive
element: either wrap the Logo+username in a LinkWithQuery (for navigation) or
render a <button> with the click handler used by SidebarMenuButton, and ensure
it keeps the same classes (size-6!, group-data-[collapsible=icon]:size-4!) and
content; if the element is purely decorative, remove cursor-pointer and any
click delegation. Also ensure the chosen element supports focus/keyboard
activation and include an accessible label/aria-label if needed.

59-76: Effect ordering is fragile — merge effect runs before reset effect on period change.

When period changes, both effects trigger in the same render:

  1. Line 60 (merge effect): appends new periodProjects to the stale list.
  2. Line 73 (reset effect): overwrites projectsToDisplay with fresh periodProjects.

The reset effect "wins" because it runs second, but the intermediate merge is wasted work and the pattern is brittle if effect order changes.

Additionally, periodProjects is read inside the reset effect (line 75) but not listed in its dependency array (line 76). This works under suspense guarantees but will trigger an exhaustive-deps lint warning.

Consider consolidating both effects into a single effect that distinguishes "new page loaded" from "period/range changed":

♻️ Consolidation sketch

One approach: track whether the period/range changed and branch accordingly in a single effect.

+  // Track previous period + range to detect resets
+  const periodKey = period === "Custom Range"
+    ? `${customRange.start}-${customRange.end}`
+    : period;
+  const [prevPeriodKey, setPrevPeriodKey] = useState(periodKey);
+
+  useEffect(() => {
+    if (periodKey !== prevPeriodKey) {
+      // Period changed → full reset
+      setPage(1);
+      setProjectsToDisplay(periodProjects);
+      setPrevPeriodKey(periodKey);
+    } else {
+      // Same period, new page → merge
+      setProjectsToDisplay((prev) => {
+        const displayed = new Set(prev.map((p) => p.name));
+        return [...prev, ...periodProjects.filter((e) => !displayed.has(e.name))];
+      });
+    }
+  }, [periodProjects, periodKey, prevPeriodKey]);
-
-  const [projectsToDisplay, setProjectsToDisplay] = useState(periodProjects);
-  useEffect(() => {
-    setProjectsToDisplay((prev) => {
-      const displayedProjectNames = new Set(prev.map((p) => p.name));
-      return [
-        ...prev,
-        ...periodProjects.filter(
-          (entry) => !displayedProjectNames.has(entry.name),
-        ),
-      ];
-    });
-  }, [periodProjects]);
-
-  // Reset page when period or date range changes
-  useEffect(() => {
-    setPage(1);
-    setProjectsToDisplay(periodProjects);
-  }, [period, customRange.start, customRange.end]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/app-sidebar.tsx` around lines 59 - 76,
The two useEffect hooks that update projectsToDisplay are fragile and do
redundant work; replace them with a single useEffect that depends on
periodProjects, period, customRange.start, and customRange.end, and inside it
branch: when period or customRange changed call setPage(1) and
setProjectsToDisplay(periodProjects) (reset path), otherwise merge new entries
from periodProjects into the existing projectsToDisplay state (merge path),
ensuring you read periodProjects only from the effect deps to satisfy
exhaustive-deps and reference the existing state via the functional updater
(setProjectsToDisplay(prev => ...)) and preserve the current merge logic that
builds a Set of existing names.

41-57: Parallelize two sequential useSuspenseQuery calls to prevent request waterfall.

The projects query (line 43) suspends until resolved before the component renders and reaches the user query (line 80), serializing two independent network requests. Use useSuspenseQueries to initiate both queries concurrently instead.

♻️ Refactor: parallelize with useSuspenseQueries
-  const {
-    data: { periodProjects, hasNext },
-  } = useSuspenseQuery(
-    trpc.analytics.projects.getPeriodProjects.queryOptions(
-      period === "Custom Range"
-        ? {
-            start: customRange.start,
-            end: customRange.end,
-            page,
-          }
-        : {
-            start: PERIODS_CONFIG[period].start,
-            end: PERIODS_CONFIG[period].end,
-            page,
-          },
-    ),
-  );
   ...
-  const {
-    data: { email, username },
-  } = useSuspenseQuery(trpc.auth.getUser.queryOptions());
+  const [projectsResult, userResult] = useSuspenseQueries({
+    queries: [
+      trpc.analytics.projects.getPeriodProjects.queryOptions(
+        period === "Custom Range"
+          ? { start: customRange.start, end: customRange.end, page }
+          : { start: PERIODS_CONFIG[period].start, end: PERIODS_CONFIG[period].end, page },
+      ),
+      trpc.auth.getUser.queryOptions(),
+    ],
+  });
+
+  const { periodProjects, hasNext } = projectsResult.data;
+  const { email, username } = userResult.data;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/app-sidebar.tsx` around lines 41 - 57,
The projects query using useSuspenseQuery
(trpc.analytics.projects.getPeriodProjects.queryOptions with
PERIODS_CONFIG/customRange and page) is being awaited before the later user
query, causing a request waterfall; replace the two sequential useSuspenseQuery
calls with a single useSuspenseQueries call that runs both queries in parallel
(one entry built from
trpc.analytics.projects.getPeriodProjects.queryOptions(...) using
period/PERIODS_CONFIG/customRange/page and the other entry for the user query),
then destructure the returned array to get periodProjects/hasNext and the user
data so the component can render once both resolve concurrently.
apps/dashboard/src/features/files-circle-packing-chart/components/circular-packing.tsx (1)

34-45: Wrap resetSVGDimensions in useCallback to fix memoization and stale effect reference.

Two issues with the current approach:

  1. resetSVGDimensions is recreated every render, defeating the memo on OptionsSection (it receives a new function reference each time).
  2. The useEffect on line 39 captures the initial render's closure and never updates the listener (empty deps []). It works by coincidence here since only stable refs and globals are read, but it will trigger an exhaustive-deps lint warning.

Both are solved by wrapping in useCallback:

♻️ Proposed fix
-import { RefObject, useEffect, useState } from "react";
+import { RefObject, useCallback, useEffect, useState } from "react";
-  const resetSVGDimensions = () => {
+  const resetSVGDimensions = useCallback(() => {
     setWidth(parentDivRef.current?.clientWidth ?? (window.innerWidth * 5) / 6);
     setHeight((window.innerWidth * 2) / 3);
-  };
+  }, [parentDivRef]);

   useEffect(() => {
     window.addEventListener("resize", resetSVGDimensions);

     return () => {
       window.removeEventListener("resize", resetSVGDimensions);
     };
-  }, []);
+  }, [resetSVGDimensions]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/dashboard/src/features/files-circle-packing-chart/components/circular-packing.tsx`
around lines 34 - 45, The resetSVGDimensions function is recreated each render
and causes a stale listener and memo invalidation for OptionsSection; wrap
resetSVGDimensions in useCallback (e.g., const resetSVGDimensions =
useCallback(() => { ... }, [parentDivRef.current, setWidth, setHeight])) and
then keep the useEffect that adds/removes the resize listener but include
resetSVGDimensions in its dependency array so the listener always references the
memoized callback; ensure the callback reads parentDivRef.current safely and
uses setWidth/setHeight from props/state in its deps.
apps/dashboard/src/components/layout/layout.tsx (1)

24-33: Consider using <SidebarInset> instead of a plain <div className="flex-1">.

Since AppSidebar uses variant="inset", the layout should wrap the main content area with <SidebarInset>. This component applies responsive styling via peer-data selectors (margins, rounded corners, shadows) that coordinate with the sidebar's collapsed/expanded states, providing proper spacing and visual cohesion between the sidebar and content area.

Import SidebarInset from @repo/ui/components/ui/sidebar and replace the <div className="flex-1"> wrapper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/layout.tsx` around lines 24 - 33,
Replace the plain wrapper div around the main content with the SidebarInset
component so the sidebar's inset variant and peer-data responsive styling apply:
import SidebarInset from `@repo/ui/components/ui/sidebar`, replace the <div
className="flex-1"> that currently contains <Header />, the <div id="loader"
...> (with isLoading, GlobalSpinner, Outlet), <ScrollToTopButton /> and <Footer
/> with <SidebarInset className="flex-1"> ... </SidebarInset>, preserving the
id="loader" inner div and existing children (Header, GlobalSpinner/Outlet,
ScrollToTopButton, Footer) and any className logic (cn(isLoading &&
"opacity-70")).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/dashboard/src/components/common/link-with-query.tsx`:
- Around line 4-18: The LinkWithQuery component is overcomplicated and fragile:
remove the useState and useEffect that read window.location.search and instead
read the reactive value from useLocation() directly. Inside LinkWithQuery, call
useLocation() (e.g., const { search } = useLocation()) and use that search when
constructing the Link (to={to + search}); delete the setSearch/useEffect logic
and any references to window.location.search so the component updates
immediately on navigation and avoids stale renders and linter warnings.

In `@apps/dashboard/src/features/files-list/components/files.tsx`:
- Around line 37-39: The languagesToFetch array is recreated each render which
makes the Files component's useEffect (the one that calls setPage(1) with
[period, customRange.start, customRange.end, languagesToFetch]) fire every time;
fix this by memoizing the languagesToFetch computation in the parent FilesList
component using useMemo so it only changes when selectedEntries changes (i.e.,
compute languagesToFetch via useMemo(() => selectedEntries.length ?
selectedEntries.map(e => e.languageSlug) : undefined, [selectedEntries])) so the
Files component receives a stable reference and pagination no longer resets.

In
`@apps/dashboard/src/features/period-languages-chart/components/period-languages-chart.tsx`:
- Around line 58-60: The Tailwind breakpoint max-[18rem] on the <h2> in
period-languages-chart.tsx will never match real viewports; update the heading’s
responsive class (the <h2 className="... max-[18rem]:text-base">) to a practical
breakpoint or use a container query: either replace max-[18rem]:text-base with a
meaningful viewport breakpoint like max-sm:text-base or max-[20rem]:text-base,
or implement a CSS container query (make the chart panel a container and apply
an element-width rule to reduce font-size) so the rule fires when the component
is narrow rather than relying on the tiny viewport breakpoint.

In `@apps/dashboard/src/features/period-projects/components/period-projects.tsx`:
- Line 12: LinkWithQuery uses useState(useLocation().search) and a
window.location.search effect which is fragile; remove the local state and
effect and instead read the current search directly from the router hook: call
useLocation() inside the LinkWithQuery component (e.g., const { search } =
useLocation()) and use that search value when building the link href/target,
preserving any existing prop merging logic; delete the window.location.search
dependency and state initialization (and related setState) so the component
reacts to location changes via useLocation() only.

In `@packages/ui/src/components/ui/sheet.tsx`:
- Around line 72-78: The close button currently uses SheetPrimitive.Close
wrapping Icon (XIcon) which discards the <span className="sr-only">Close</span>,
leaving the button without an accessible name; update the SheetPrimitive.Close
(or the component that renders the actual button) to include an
aria-label="Close" (or aria-label prop driven by showCloseButton) so screen
readers get a name, and remove the sr-only span; also revise the positioning
class on the Icon/SheetPrimitive.Close from "absolute bottom-14" to a top-right
placement (e.g., absolute top-? right-? consistent with your design system) or
confirm and document if bottom placement is intentional. Ensure changes target
the SheetPrimitive.Close / Icon (XIcon) usage so accessibility and layout are
fixed.

In `@packages/ui/src/globals.css`:
- Around line 46-51: The CSS custom properties (--shadow-x, --shadow-y,
--shadow-blur, --shadow-spread, --shadow-opacity, --shadow-color) declared in
:root and .dark are unused; either remove both declarations to avoid dead code
or document them as intentional placeholders and wire them into your shadow
utilities (e.g., replace hardcoded values in shadow-2xs...shadow-2xl with these
variables). Locate the declarations by the exact variable names and update
globals.css accordingly, and if you choose to keep them add a comment above the
declarations explaining they are reserved for future theming or ensure the
Tailwind/shadow utility definitions reference those variables.

---

Outside diff comments:
In `@apps/dashboard/src/features/files-list/components/languages-dropdown.tsx`:
- Around line 29-35: The button rendered in the Languages dropdown (the <Button>
wrapping the "Languages" <span> and <ChevronDown />) becomes icon-only at narrow
viewports because the "Languages" span is hidden; add an accessible name by
setting an aria-label (e.g., aria-label="Languages") on that <Button> element so
assistive tech still perceives the control when the span is hidden, ensuring the
label matches the visible text and remains consistent with the <ChevronDown />
icon.

---

Nitpick comments:
In `@apps/api/src/trpc/trpc.service.ts`:
- Line 53: The default rate limit max was doubled (rateLimiterParams -> max =
600) expanding burst allowances for publicProcedure and protectedProcedure; add
observability and safer defaults: revert the implicit default back to the
previous conservative value (e.g., 300) in trpc.service.ts or require explicit
overrides, and instrument the rate-limit rejection handler (where
TOO_MANY_REQUESTS is raised) to emit a metric/alert (counter/timestamp with
labels like endpoint/procedure/IP) so ops can create an alert on spikes; update
publicProcedure/protectedProcedure callers to pass explicit key/max if they need
higher limits.

In `@apps/dashboard/src/App.tsx`:
- Around line 99-105: The SidebarProvider is mounted globally in App.tsx causing
its keyboard listener and cookie persistence to run on auth routes; move the
SidebarProvider so it only wraps the dashboard routes (e.g., wrap it around the
dashboard layout/component that renders the sidebar, such as DashboardLayout) or
conditionally render SidebarProvider in App.tsx by checking the current route
(exclude paths like login, register, forgot-password) before mounting it; adjust
references in App.tsx (remove SidebarProvider around Outlet/Wrapper) and ensure
the sidebar-related state/hooks still wrap the components that need them
(Wrapper, components inside DashboardLayout) so the global listener is not
active on AuthLayout routes.

In `@apps/dashboard/src/components/layout/app-sidebar.tsx`:
- Around line 96-99: The container div that currently renders the Logo and
{username} is marked cursor-pointer but is not keyboard-accessible because the
SidebarMenuButton (with asChild) delegates to a plain <div>; change the child to
an interactive element: either wrap the Logo+username in a LinkWithQuery (for
navigation) or render a <button> with the click handler used by
SidebarMenuButton, and ensure it keeps the same classes (size-6!,
group-data-[collapsible=icon]:size-4!) and content; if the element is purely
decorative, remove cursor-pointer and any click delegation. Also ensure the
chosen element supports focus/keyboard activation and include an accessible
label/aria-label if needed.
- Around line 59-76: The two useEffect hooks that update projectsToDisplay are
fragile and do redundant work; replace them with a single useEffect that depends
on periodProjects, period, customRange.start, and customRange.end, and inside it
branch: when period or customRange changed call setPage(1) and
setProjectsToDisplay(periodProjects) (reset path), otherwise merge new entries
from periodProjects into the existing projectsToDisplay state (merge path),
ensuring you read periodProjects only from the effect deps to satisfy
exhaustive-deps and reference the existing state via the functional updater
(setProjectsToDisplay(prev => ...)) and preserve the current merge logic that
builds a Set of existing names.
- Around line 41-57: The projects query using useSuspenseQuery
(trpc.analytics.projects.getPeriodProjects.queryOptions with
PERIODS_CONFIG/customRange and page) is being awaited before the later user
query, causing a request waterfall; replace the two sequential useSuspenseQuery
calls with a single useSuspenseQueries call that runs both queries in parallel
(one entry built from
trpc.analytics.projects.getPeriodProjects.queryOptions(...) using
period/PERIODS_CONFIG/customRange/page and the other entry for the user query),
then destructure the returned array to get periodProjects/hasNext and the user
data so the component can render once both resolve concurrently.

In `@apps/dashboard/src/components/layout/layout.tsx`:
- Around line 24-33: Replace the plain wrapper div around the main content with
the SidebarInset component so the sidebar's inset variant and peer-data
responsive styling apply: import SidebarInset from
`@repo/ui/components/ui/sidebar`, replace the <div className="flex-1"> that
currently contains <Header />, the <div id="loader" ...> (with isLoading,
GlobalSpinner, Outlet), <ScrollToTopButton /> and <Footer /> with <SidebarInset
className="flex-1"> ... </SidebarInset>, preserving the id="loader" inner div
and existing children (Header, GlobalSpinner/Outlet, ScrollToTopButton, Footer)
and any className logic (cn(isLoading && "opacity-70")).

In `@apps/dashboard/src/components/layout/navigation-reset-wrapper.tsx`:
- Around line 4-12: Rename the generic exported component Wrapper to a
descriptive name (e.g., ScrollResetWrapper or NavigationScrollReset) to reflect
its behavior; update the component declaration/export (Wrapper) to the new name
and change all import sites (for example where App.tsx imports Wrapper) to the
new identifier so references remain consistent; keep the existing implementation
(useLocation, useLayoutEffect, window.scrollTo) unchanged—only change the
component name and its imports/exports to improve clarity.

In `@apps/dashboard/src/features/auth/components/layout.tsx`:
- Around line 3-8: Rename the exported React component from Layout to AuthLayout
(change the symbol exported by the component currently declared as Layout to
AuthLayout) and update all imports/usages that currently import it (notably the
place where it was imported as Layout as AuthLayout) to import AuthLayout
directly; specifically, update the component declaration name (Layout ->
AuthLayout), its export, and any references/imports in main.tsx and other
consumers so there is no aliased import.

In
`@apps/dashboard/src/features/files-circle-packing-chart/components/circular-packing.tsx`:
- Around line 34-45: The resetSVGDimensions function is recreated each render
and causes a stale listener and memo invalidation for OptionsSection; wrap
resetSVGDimensions in useCallback (e.g., const resetSVGDimensions =
useCallback(() => { ... }, [parentDivRef.current, setWidth, setHeight])) and
then keep the useEffect that adds/removes the resize listener but include
resetSVGDimensions in its dependency array so the listener always references the
memoized callback; ensure the callback reads parentDivRef.current safely and
uses setWidth/setHeight from props/state in its deps.

In
`@apps/dashboard/src/features/files-circle-packing-chart/hooks/use-animate-chart.tsx`:
- Around line 42-44: Remove the unnecessary useMemo wrappers around the trivial
multiplications: replace the computed values derived in boundaryHeightMargin and
boundaryWidthMargin (currently using useMemo with height * 1e-9 and width *
1e-9) with plain const assignments, and then remove useMemo from the import list
if it has no other usages in the file; update references to boundaryHeightMargin
and boundaryWidthMargin as needed.

In `@apps/dashboard/src/loaders/auth-loader.ts`:
- Around line 102-109: Replace the manual concatenation of Zod issue messages in
googleAuthLoader with the centralized formatter: when
validatedCallBackUrl.success is false, throw new
Error(formatZodError(validatedCallBackUrl.error)) instead of using
issues.reduce(...); ensure the function uses the existing formatZodError helper
(import or reference the same symbol used by
redirectToVSCodeAfterGoogleAuthLoader) so both loaders share consistent Zod
error formatting (note the thrown error is caught by the surrounding try/catch
in googleAuthLoader).

In `@packages/ui/src/components/ui/sidebar.tsx`:
- Around line 80-85: The cookie assignment for SIDEBAR_COOKIE_NAME in the effect
using setOpenProp/open should explicitly include SameSite and Secure attributes:
append "; SameSite=Lax" (or your chosen policy) to the cookie string and add ";
Secure" when the app is served over HTTPS (detect via location.protocol ===
'https:' or window.isSecureContext) so the code that sets document.cookie with
SIDEBAR_COOKIE_NAME and SIDEBAR_COOKIE_MAX_AGE includes these flags.

- wrapped `langagesToFetch` in a `useMemo` in the `files-list.tsx` component
- fixed the issue of the scroll to top button being moved when the dropdowns/modals are toggled by moving in the root `App.tsx` file instead of it being a part of the layout (affected by the sidebar)
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
apps/dashboard/src/components/layout/layout.tsx (1)

26-29: Loading state renders spinner and Outlet simultaneously, causing large page height.

When isLoading is true, GlobalSpinner (h-dvh — full viewport height) and <Outlet /> both render inside the same div. This stacks them vertically, producing a very tall scrollable area during route transitions. Consider conditionally rendering only the spinner while loading, or positioning the spinner as an overlay.

♻️ Option A: render only the spinner while loading
-        <div id="loader" className={cn(isLoading && "opacity-70")}>
-          {isLoading && <GlobalSpinner />}
-          <Outlet />
-        </div>
+        <div id="loader" className={cn(isLoading && "opacity-70")}>
+          {isLoading ? <GlobalSpinner /> : <Outlet />}
+        </div>
♻️ Option B: overlay the spinner on top of existing content
-        <div id="loader" className={cn(isLoading && "opacity-70")}>
-          {isLoading && <GlobalSpinner />}
-          <Outlet />
-        </div>
+        <div id="loader" className={cn("relative", isLoading && "opacity-70")}>
+          {isLoading && (
+            <div className="absolute inset-0 z-10 flex items-center justify-center">
+              <ClipLoader size={80} color="var(--primary)" />
+            </div>
+          )}
+          <Outlet />
+        </div>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/dashboard/src/components/layout/layout.tsx` around lines 26 - 29, The
loader div currently renders both GlobalSpinner and Outlet when isLoading is
true, causing the page to grow tall; update the layout so that when isLoading is
true you render only the spinner (or render the spinner as an absolutely
positioned overlay) — e.g., in the component that uses isLoading, GlobalSpinner
and Outlet, change the JSX around the div with id="loader" (and the cn(...)
usage) so that it conditionally returns <GlobalSpinner /> alone while isLoading,
otherwise returns the normal <Outlet />; alternatively implement overlay
behavior by wrapping Outlet in a relatively positioned container and rendering
GlobalSpinner with absolute positioning (h-dvh, inset-0, flex center) so the
Outlet remains underneath without increasing document height. Ensure references
to isLoading, GlobalSpinner, Outlet and the div id="loader" are updated
accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/ui/src/components/scroll-to-top-button.tsx`:
- Around line 29-39: The wrapper div in ScrollToTopButton uses
"pointer-events-none", which prevents the nested Button's onClick (jumpToTop)
from firing; fix by re-enabling pointer events on the interactive element — add
"pointer-events-auto" to the Button's className (or remove the wrapper's
pointer-events-none if that wrapper behavior is unnecessary) so the Button can
receive clicks and jumpToTop will be invoked.

---

Nitpick comments:
In `@apps/dashboard/src/components/layout/layout.tsx`:
- Around line 26-29: The loader div currently renders both GlobalSpinner and
Outlet when isLoading is true, causing the page to grow tall; update the layout
so that when isLoading is true you render only the spinner (or render the
spinner as an absolutely positioned overlay) — e.g., in the component that uses
isLoading, GlobalSpinner and Outlet, change the JSX around the div with
id="loader" (and the cn(...) usage) so that it conditionally returns
<GlobalSpinner /> alone while isLoading, otherwise returns the normal <Outlet
/>; alternatively implement overlay behavior by wrapping Outlet in a relatively
positioned container and rendering GlobalSpinner with absolute positioning
(h-dvh, inset-0, flex center) so the Outlet remains underneath without
increasing document height. Ensure references to isLoading, GlobalSpinner,
Outlet and the div id="loader" are updated accordingly.

- re-enabled the click on the scroll-to-top-button by adding pointer-events-auto to the button
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@packages/ui/src/components/scroll-to-top-button.tsx`:
- Around line 29-39: The wrapper div in the ScrollToTopButton component had
pointer-events disabled; ensure the Button inside (component Button in
scroll-to-top-button.tsx) has pointer-events restored so clicks reach the
handler jumpToTop — keep the outer div className "pointer-events-none ..." and
ensure the Button has "pointer-events-auto" (and retains aria-label/title and
onClick={jumpToTop}) so the click/tap interactivity is restored while
transparent areas remain non-interactive.

@Friedrich482 Friedrich482 merged commit 36971e8 into main Feb 18, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant