Skip to content

BREAKING CHANGE(#196): Migration to Tanstack Router from Next JS#197

Merged
yesyash merged 27 commits intodevelopfrom
anuj/migrate-to-tanstack
Sep 11, 2025
Merged

BREAKING CHANGE(#196): Migration to Tanstack Router from Next JS#197
yesyash merged 27 commits intodevelopfrom
anuj/migrate-to-tanstack

Conversation

@AnujChhikara
Copy link
Contributor

@AnujChhikara AnujChhikara commented Sep 5, 2025

Date: 5 Sep 2025

Developer Name: @AnujChhikara


Issue Ticket Number

Description

  • Migrated the app from next js to react + tankstack router.
  • using file based routing.
  • removed the stroybook dependencies as not using it right now.

Documentation Updated?

  • Yes
  • No

Under Feature Flag

  • Yes
  • No

Database Changes

  • Yes
  • No

Breaking Changes

  • Yes
  • No

Development Tested?

  • Yes
  • No

Screenshots

Screenshot 1

Test Coverage

Screenshot 1

Additional Notes

Description by Korbit AI

What change is being made?

Migrate from Next.js router to Tanstack Router, refactoring directory structure, replacing environment variables with Vite style variables, updating ESLint configurations, and removing Storybook integration.

Why are these changes being made?

The migration to the Tanstack Router framework provides more flexibility and modern routing capabilities aligning with updated project requirements. This also necessitated updating the environment variable strategy to align with Vite, improving build processes. Removing Storybook reduces maintenance overhead and reflects the adoption of new component testing approaches.

Is this description stale? Ask me to generate a new description by commenting /korbit-generate-pr-description

@AnujChhikara AnujChhikara self-assigned this Sep 5, 2025
@korbit-ai
Copy link

korbit-ai bot commented Sep 5, 2025

Based on your review schedule, I'll hold off on reviewing this PR until it's marked as ready for review. If you'd like me to take a look now, comment /korbit-review.

Your admin can change your review schedule in the Korbit Console

@coderabbitai
Copy link

coderabbitai bot commented Sep 5, 2025

Important

Review skipped

Auto incremental reviews are disabled on this repository.

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

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

Summary by CodeRabbit

  • New Features

    • Introduced Vite SPA entry (index.html) and TanStack Router-powered navigation.
    • Added top navigation component and a new Dashboard Welcome screen.
    • Added Team Members view with search and actions.
    • Updated “Include Done” toggle behavior on dashboard and teams.
  • Refactor

    • Migrated routing from Next.js App Router to TanStack Router across the app.
    • Removed Storybook and related files; streamlined UI component imports.
    • Switched environment variables to VITE_*.
  • Documentation

    • README updated for new routing, structure, commands; Storybook docs removed.
  • Chores

    • Updated CI, ESLint config, .gitignore, and package scripts/dependencies for Vite.

Walkthrough

Migrates frontend from Next.js to Vite + TanStack Router, removes App Router pages and Storybook, updates environment variables and CI, introduces index.html and a new React entry (src/main.tsx), adjusts MSW setup, refactors routing/navigation, updates APIs/types (teams, tasks, users), and reorganizes components/imports with many client-directive removals.

Changes

Cohort / File(s) Summary
Env & Config Migration
/.env.sample, /next.config.ts (removed), /eslint.config.mjs, /index.html, /src/lib/api-client.ts, /src/config/app-config.ts
Switch to Vite env vars (VITE_*), remove Next config, adopt React ESLint presets, add SPA index.html, update axios baseURL to import.meta.env, adapt app config to Vite envs.
Build, CI, Ignore Rules
.github/workflows/build-and-test.yml, /.gitignore, /package.json
CI switches to Vite env var, removes Storybook step; ignore rules updated; package scripts/deps move from Next/Storybook to Vite + TanStack Router; add routes:generate, preview, check.
Remove Storybook
.storybook/* (removed), components/Shimmer/*.stories.tsx (removed)
Deletes Storybook config and stories.
Remove Next App Router pages/layout
/app/** (multiple pages/layout deleted)
Deletes App Router pages and layouts for root, dashboard, teams, and related routes.
App Bootstrap & MSW
/src/main.tsx (new), /src/mocks/setup.ts (new), /components/msw-provider.tsx (removed), /__mocks__/init.ts (removed)
New React 18 entry with RouterProvider; centralized enableMocking in src/mocks/setup; removes MSW provider/init modules.
Routing Migration (TanStack Router)
/src/components/layout/*, /src/components/users/*, /src/modules/dashboard/**, /src/modules/admin/**, /src/components/todos/**, /src/components/teams/*
Replace Next.js Link/router/search hooks with TanStack Link/useNavigate/useSearch/useLocation; update route/search handling across navigation, dashboard tabs/tables, admin, teams.
UI Components refactor & client directive removal
/src/components/ui/*, /src/components/common/**, /src/components/layout/**, /src/components/users/**, /src/components/todos/**
Widespread removal of 'use client'; import path reorganizations; minor export order tweaks; one export removed: SheetDescription.
APIs & Types
/src/api/common/common.types.ts, /src/api/**
Remove TApiMethodsRecord/TApiFunction; drop satisfies constraints on Api objects; teams types expanded (TTeamUser, users field) and new TTeamDto; createTeam returns TTeamDto.
Teams Feature
/src/components/teams/team-activity.tsx, /src/components/teams/team-members.tsx (new), related imports
Update params via TanStack useParams; add TeamMembers component with search/filter/table; path fixes for shared components.
Dashboard Feature
/src/modules/dashboard/components/*, /src/modules/dashboard/index.tsx
Migrate query handling to TanStack; new DashboardWelcomeScreen; adjust tabs/search/status flow; shimmer/import path fixes.
Todos Feature
/src/components/todos/*, /src/lib/todo-util.ts, /src/lib/team-utils.ts
Move todo components under todos/; new IncludeDoneSwitch with TanStack navigation; table/search logic adapted to search object; cache invalidations improved in watchlist-button.
Search Hook & Component
/src/hooks/useSearch.ts, /components/SearchComponent.tsx (removed), /src/components/common/searchbar.tsx
SearchComponent removed; hook typed result data as TUser with safer error handling; input import path fix.
Mocks & Handlers
/src/mocks/data/*.mock.ts, /src/mocks/handlers/*.handler.ts
Mock data adjusted (teams.users, watchlist.createdBy object); handlers simplified to ignore params/body; watchlist add signature updated; minor const change.
Docs
/README.md
Document migration to TanStack Router/Vite, new structure, commands; remove Storybook instructions.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant B as Browser
  participant M as enableMocking()
  participant R as React Root
  participant RP as RouterProvider
  participant P as Providers

  B->>M: import and call enableMocking()
  alt Dev && Mocking enabled
    M-->>B: start MSW worker
  else Otherwise
    M-->>B: no-op
  end
  B->>R: createRoot(#root)
  R->>P: render Providers
  P->>RP: render RouterProvider(router)
  RP-->>B: App UI mounted
Loading
sequenceDiagram
  autonumber
  participant U as User
  participant T as IncludeDoneSwitch
  participant NR as TanStack Router
  participant D as Dashboard

  U->>T: Toggle switch
  T->>NR: navigate to "/dashboard" with search { status, tab, search }
  NR-->>D: Provide updated search object
  D->>D: Fetch/query tasks with new filters
  D-->>U: Render updated task list
Loading
sequenceDiagram
  autonumber
  participant C as Component
  participant L as useLocation()/useSearch()
  participant N as useNavigate()
  participant Q as React Query

  C->>L: read pathname/search
  C->>Q: query using search params
  U->>C: action (tab/search)
  C->>N: navigate({ to, search: updater })
  N-->>C: new search provided
  C->>Q: refetch with new keys
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related issues

Possibly related PRs

Suggested labels

feature task

Suggested reviewers

  • iamitprakash
  • Achintya-Chatterjee
  • MayankBansal12

Poem

A hop, a skip—goodbye, Next’s old road,
Vite winds whirr where our packets load.
Routes now tan-stacked, swift as breeze,
MSW whispers: “mock me, please.”
Tasks align, teams march in queue—
Thump-thump! says the rabbit: “Ship’s anew.” 🐇✨

✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch anuj/migrate-to-tanstack

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.

@AnujChhikara AnujChhikara marked this pull request as ready for review September 5, 2025 21:42
@korbit-ai
Copy link

korbit-ai bot commented Sep 5, 2025

Korbit doesn't automatically review large (3000+ lines changed) pull requests such as this one. If you want me to review anyway, use /korbit-review.

@coderabbitai coderabbitai bot added the feature task A big ticket item that needs to come up as a feature label Sep 5, 2025
@Hariom01010
Copy link
Contributor

Hariom01010 commented Sep 5, 2025

I have done regression testing of the migration on call with @AnujChhikara and all the functionalities are working as expected.

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: 71

Caution

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

⚠️ Outside diff range comments (80)
src/lib/team-utils.ts (1)

41-41: Avoid “undefined” in UI when status keys are unknown; add a safe accessor.

Indexing with casts can render “from undefined to undefined” if backend sends an unmapped status. Use a helper with a fallback.

Apply within this line:

-        description: `${activity.performed_by_name} changed status of ${activity.task_title} from ${TASK_STATUS_TO_TEXT_MAP[activity.status_from as keyof typeof TASK_STATUS_TO_TEXT_MAP]} to ${TASK_STATUS_TO_TEXT_MAP[activity.status_to as keyof typeof TASK_STATUS_TO_TEXT_MAP]}`,
+        description: `${activity.performed_by_name} changed status of ${activity.task_title} from ${getStatusText(activity.status_from)} to ${getStatusText(activity.status_to)}`,

Add this helper near the top of the file:

type StatusKey = keyof typeof TASK_STATUS_TO_TEXT_MAP;
export function getStatusText(status: unknown): string {
  const key = String(status) as StatusKey;
  return TASK_STATUS_TO_TEXT_MAP[key] ?? key;
}
src/components/teams/team-activity.tsx (1)

11-15: Route ID correct; guard the query

  • The string / _internal/teams/$teamId exactly matches the generated route ID in src/routes/_internal.teams.$teamId.tsx and routeTree.gen.ts, so no update is needed.
  • Optional: in src/components/teams/team-activity.tsx, add enabled: !!teamId to your useQuery call to prevent it running when teamId is undefined:
   const { data, isLoading, isError } = useQuery({
     queryKey: TeamsApi.getTeamActivities.key({ teamId }),
     queryFn: () => TeamsApi.getTeamActivities.fn({ teamId }),
+    enabled: !!teamId,
   })
src/modules/dashboard/components/dashboard-shimmer.tsx (1)

5-15: Add basic a11y for skeletons.

Expose loading state and hide decorative blocks from SRs.

-    <div className="px-4 md:px-6">
+    <div className="px-4 md:px-6" role="status" aria-live="polite" aria-busy="true">
       <div className="flex flex-col items-center space-y-1 py-12">
-        <Shimmer className="h-6 w-32" />
-        <Shimmer className="h-6 w-48" />
+        <Shimmer className="h-6 w-32" aria-hidden="true" />
+        <Shimmer className="h-6 w-48" aria-hidden="true" />
       </div>

       <div className="grid grid-cols-1 gap-6 xl:grid-cols-12">
-        <Shimmer className="h-48 xl:col-span-8 2xl:col-span-9" />
-        <Shimmer className="h-48 xl:col-span-4 2xl:col-span-3" />
+        <Shimmer className="h-48 xl:col-span-8 2xl:col-span-9" aria-hidden="true" />
+        <Shimmer className="h-48 xl:col-span-4 2xl:col-span-3" aria-hidden="true" />
       </div>
src/components/teams/team-user-search-dropdown.tsx (3)

34-38: Guard the query and harden selection.

Avoid fetching with empty teamId; default to [] to simplify consumers.

-  const { data: usersList, isLoading } = useQuery({
+  const { data: usersList = [], isLoading, isFetching } = useQuery({
     queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
     queryFn: () => TeamsApi.getTeamById.fn({ teamId, member: true }),
-    select: (res) => res.users,
+    select: (res) => res.users ?? [],
+    enabled: !!teamId,
   })

51-62: Combobox a11y: wire ARIA and listbox linkage; minor UX polish.

Make the trigger a proper combobox, link to the list, and hide decorative icons from SRs. Disable while loading.

         <Button
           variant="outline"
           role="combobox"
+          aria-haspopup="listbox"
+          aria-controls="team-user-listbox"
           aria-expanded={open}
+          aria-label={selectedUser?.name ?? placeholder}
           className={cn(
             'w-full justify-between font-normal',
             !selectedUser && 'text-muted-foreground',
           )}
+          disabled={isLoading}
         >
           {selectedUser?.name ?? placeholder}
-          <ChevronDown className="opacity-50" />
+          <ChevronDown className="opacity-50" aria-hidden="true" />
         </Button>
-          <CommandList>
+          <CommandList id="team-user-listbox">
-            <CommandEmpty>{isLoading ? 'Searching users...' : 'No users found.'}</CommandEmpty>
+            <CommandEmpty>
+              {(isLoading || isFetching) ? 'Searching users...' : 'No users found.'}
+            </CommandEmpty>
-                  <User className="text-muted-foreground h-4 w-4" />
+                  <User className="text-muted-foreground h-4 w-4" aria-hidden="true" />

Also applies to: 73-75, 84-85


70-93: Optional: support keyboard type-ahead without controlled state.

cmdk filters by item text/value; you can drop local search state unless you need it elsewhere.

If you don’t need the current search elsewhere:

-  const [search, setSearch] = useState('')
+  // Uncontrolled filtering via cmdk; no local state needed.

-          <CommandInput placeholder={placeholder} value={search} onValueChange={setSearch} />
+          <CommandInput placeholder={placeholder} />
src/components/common/shimmer/ListShimmer.tsx (1)

11-14: Expose loading semantics; hide decorative items.

Improves screen-reader experience for skeleton lists.

-    <div className={cn('flex h-24 flex-col gap-2', className)} data-testid="list-shimmer">
+    <div
+      className={cn('flex h-24 flex-col gap-2', className)}
+      data-testid="list-shimmer"
+      role="status"
+      aria-live="polite"
+      aria-busy="true"
+    >
-      {[...Array(count)].map((_, index) => (
-        <Shimmer key={index} />
+      {[...Array(count)].map((_, index) => (
+        <Shimmer key={index} aria-hidden="true" />
       ))}
src/modules/landing-page/landing-footer.tsx (3)

45-50: Hide decorative icon from SRs.

-                      <ArrowUpRight className="ml-1 h-3 w-3 opacity-0 transition-opacity group-hover:opacity-100" />
+                      <ArrowUpRight className="ml-1 h-3 w-3 opacity-0 transition-opacity group-hover:opacity-100" aria-hidden="true" />

59-59: Avoid hard-coded copyright year.

Keeps the footer evergreen.

-          <p className="text-sm text-gray-600">© 2025 {appConfig.appName}. All rights reserved.</p>
+          <p className="text-sm text-gray-600">© {new Date().getFullYear()} {appConfig.appName}. All rights reserved.</p>

42-53: Replace placeholder <a href="#"> tags with TanStack Router <Link>
In src/modules/landing-page/landing-footer.tsx (lines 43–47, 62–66), swap each <a href="#">…</a> for a <Link> from TanStack Router to enable proper routing, prefetching, and history integration.

src/modules/landing-page/landing-dashboard-overview.tsx (4)

134-136: Add alt text to avatar image.

Improves a11y without changing visuals.

-              <Avatar>
-                <AvatarImage src="/img/user-2.jpg" />
+              <Avatar>
+                <AvatarImage src="/img/user-2.jpg" alt="User avatar" />
                 <AvatarFallback>PC</AvatarFallback>
               </Avatar>

140-146: Fix copy and tone in greeting.

Generic copy + pluralization.

-                  <h1 className="text-lg font-medium text-black transition-all duration-200 md:text-xl">
-                    Good Morning, Prakash Sir
-                  </h1>
+                  <h1 className="text-lg font-medium text-black transition-all duration-200 md:text-xl">
+                    Good morning
+                  </h1>
                 </div>
-                <p className="text-xs text-gray-600 transition-all duration-200 md:text-sm">
-                  You have completed 9 task today 👏
-                </p>
+                <p className="text-xs text-gray-600 transition-all duration-200 md:text-sm">
+                  You have completed 9 tasks today 👏
+                </p>

234-238: Show “You” as provided; avoid unrelated initials.

The current fallback shows “JD” when assignee is “You”.

-                      <Avatar className="h-6 w-6">
-                        <AvatarFallback className="bg-gray-100 text-xs">
-                          {task.assignee === 'You' ? 'JD' : task.assignee}
-                        </AvatarFallback>
-                      </Avatar>
+                      <Avatar className="h-6 w-6">
+                        <AvatarFallback className="bg-gray-100 text-xs">
+                          {task.assignee}
+                        </AvatarFallback>
+                      </Avatar>

22-68: Hoist static demo data outside the component to avoid re-allocations.

Micro-optimization and cleaner diffs.

Example:

// Move above component
const TODAY_TASKS = [ /*...*/ ] as const
const UPCOMING_TASKS = [ /*...*/ ] as const

export const DashboardPreview = () => {
  // ...
  return (
    // replace todayTasks/upcomingTasks with TODAY_TASKS/UPCOMING_TASKS
  )
}

Also applies to: 70-74

src/api/auth/auth.api.ts (1)

3-10: Preserve API object shape and literal key types (use const tuple + explicit contract).

Dropping the previous satisfies removes compile-time checks on the API surface. Also, ['AuthApi.logout'] is inferred as string[], losing literal type info.

Apply:

-export const AuthApi = {
+type TApiMethod<T = unknown, TArgs extends any[] = []> = {
+  key: readonly unknown[]
+  fn: (...args: TArgs) => Promise<T>
+}
+
+export const AuthApi: { logout: TApiMethod<void> } = {
   logout: {
-    key: ['AuthApi.logout'],
+    key: ['AuthApi.logout'] as const,
     fn: async (): Promise<void> => {
       await apiClient.post('/v1/auth/logout')
     },
   },
 }
src/components/ui/label.tsx (1)

6-17: Consider forwarding refs to preserve Radix interop.

Wrapping a Radix primitive without forwardRef prevents consumers from attaching refs to the underlying element.

If desired:

-function Label({ className, ...props }: React.ComponentProps<typeof LabelPrimitive.Root>) {
-  return (
-    <LabelPrimitive.Root
+const Label = React.forwardRef<
+  React.ElementRef<typeof LabelPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
+>(({ className, ...props }, ref) => {
+  return (
+    <LabelPrimitive.Root
+      ref={ref}
       data-slot="label"
       className={cn(
         'flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50',
         className,
       )}
       {...props}
     />
-  )
-}
+  )
+})
src/components/ui/switch.tsx (1)

6-24: Optional: forward refs and ensure labeling.

Forwarding refs keeps Radix semantics; also ensure consumers pair this with a or aria-label.

-function Switch({ className, ...props }: React.ComponentProps<typeof SwitchPrimitive.Root>) {
-  return (
-    <SwitchPrimitive.Root
+const Switch = React.forwardRef<
+  React.ElementRef<typeof SwitchPrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof SwitchPrimitive.Root>
+>(({ className, ...props }, ref) => {
+  return (
+    <SwitchPrimitive.Root
+      ref={ref}
       data-slot="switch"
       className={cn(
         'peer data-[state=checked]:bg-primary data-[state=unchecked]:bg-input focus-visible:border-ring focus-visible:ring-ring/50 dark:data-[state=unchecked]:bg-input/80 inline-flex h-[1.15rem] w-8 shrink-0 items-center rounded-full border border-transparent shadow-xs transition-all outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50',
         className,
       )}
       {...props}
     >
       <SwitchPrimitive.Thumb
         data-slot="switch-thumb"
         className={cn(
           'bg-background dark:data-[state=unchecked]:bg-foreground dark:data-[state=checked]:bg-primary-foreground pointer-events-none block size-4 rounded-full ring-0 transition-transform data-[state=checked]:translate-x-[calc(100%-2px)] data-[state=unchecked]:translate-x-0',
         )}
       />
     </SwitchPrimitive.Root>
-  )
-}
+  )
+})
src/api/users/users.api.ts (5)

19-20: Unstable/colliding query keys: avoid Object.values-flattened keys.

...Object.values(params || {}) can reorder/collapse values and collide across different param shapes. Use the object itself in the key.

-    key: (params?: TUsersSearchParams) => ['usersApi.users', ...Object.values(params || {})],
+    key: (params?: TUsersSearchParams) => ['usersApi.users', params ?? null] as const,

20-23: Type the axios call to enforce response shape.

Without a generic, data may degrade to any and bypass your return type.

-    fn: async (params?: TUsersSearchParams): Promise<TApiResponse<TUsersSearchResponse>> => {
-      const { data } = await apiClient.get(`/v1/users`, { params })
+    fn: async (params?: TUsersSearchParams): Promise<TApiResponse<TUsersSearchResponse>> => {
+      const { data } = await apiClient.get<TApiResponse<TUsersSearchResponse>>(`/v1/users`, { params })
       return data
     },

7-8: Use const tuple for static key.

Preserves literal types for better DX with TanStack Query.

-    key: ['usersApi.getUserInfo'],
+    key: ['usersApi.getUserInfo'] as const,

5-25: Retain a minimal API-surface type contract after dropping TApiMethodsRecord.

To keep compile-time protection across modules, add a tiny local contract.

-export const UsersApi = {
+type TApiMethod<T = unknown, TArgs extends any[] = []> = {
+  key: readonly unknown[]
+  fn: (...args: TArgs) => Promise<T>
+}
+
+export const UsersApi: {
+  getUserInfo: TApiMethod<TUser>
+  users: TApiMethod<TApiResponse<TUsersSearchResponse>, [params?: TUsersSearchParams]>
+} = {

19-23: Stabilize cache key serialization and automate detection
Only one Object.values-based key was found in src/api/users/users.api.ts. Replace ...Object.values(params||{}) with a deterministic serialization (e.g. sorted key–value pairs or JSON.stringify(params)) and add a lint rule or test to flag any future instances across API definitions.

src/api/labels/labels.api.ts (2)

4-4: Fix exported name typo: LablesApi ➜ LabelsApi.

This will otherwise create broken imports and inconsistent naming across the API layer.

Apply:

-export const LablesApi = {
+export const LabelsApi = {

5-11: Align naming and cache key with returned data (plural).

The method name is singular but returns Label[] from /v1/labels; the cache key also references “LabelApi”. Unify to avoid confusion and cache collisions.

Apply:

-export const LabelsApi = {
-  getLabel: {
-    key: ['LabelApi.getLabels'],
+export const LabelsApi = {
+  getLabels: {
+    key: ['LabelsApi.getLabels'],
     fn: async (): Promise<Label[]> => {
-      const res = await apiClient.get('/v1/labels')
-      return res?.data?.labels ?? []
+      const res = await apiClient.get<{ labels: Label[] }>('/v1/labels')
+      return res.data.labels ?? []
     },
   },
 }
src/components/ui/popover.tsx (2)

26-29: Invalid Tailwind arbitrary value syntax for transform-origin.

Use origin-[var(--radix-popover-content-transform-origin)] instead of origin-(--radix-popover-content-transform-origin). Also replace non-standard outline-hidden with outline-none.

Apply:

-        className={cn(
-          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden',
-          className,
-        )}
+        className={cn(
+          'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-[var(--radix-popover-content-transform-origin)] rounded-md border p-4 shadow-md outline-none',
+          className,
+        )}

14-35: Forward refs for better composition and focus management.

Expose refs for Content to integrate with focus traps/animations; common Radix pattern.

Apply:

-function PopoverContent({
-  className,
-  align = 'center',
-  sideOffset = 4,
-  ...props
-}: React.ComponentProps<typeof PopoverPrimitive.Content>) {
-  return (
+const PopoverContent = React.forwardRef<
+  React.ElementRef<typeof PopoverPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
+>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => {
+  return (
     <PopoverPrimitive.Portal>
       <PopoverPrimitive.Content
+        ref={ref}
         data-slot="popover-content"
         align={align}
         sideOffset={sideOffset}
         className={cn(
           'bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-[var(--radix-popover-content-transform-origin)] rounded-md border p-4 shadow-md outline-none',
           className,
         )}
         {...props}
       />
     </PopoverPrimitive.Portal>
-  )
-}
+  )
+})
+PopoverContent.displayName = 'PopoverContent'
src/mocks/data/tasks.mock.ts (2)

410-416: Status filter bug: requesting DONE returns unfiltered tasks.

When params.status === 'DONE' you skip filtering, returning mixed statuses.

Apply:

-    if (params?.status) {
-      if (params.status !== 'DONE') {
-        filteredTasks = filteredTasks.filter((task) => task.status === params.status)
-      }
-    } else {
+    if (params?.status) {
+      filteredTasks = filteredTasks.filter((task) => task.status === params.status)
+    } else {
       filteredTasks = filteredTasks.filter((task) => task.status !== TASK_STATUS_ENUM.DONE)
     }

454-456: Use slice or crypto.randomUUID() instead of deprecated substr().

Modernize ID generation in mocks.

Apply:

-    const taskId = `task_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
-    const assigneeId = `assignee_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
+    const rnd = Math.random().toString(36).slice(2, 11)
+    const taskId = (globalThis.crypto?.randomUUID?.() ?? `task_${Date.now()}_${rnd}`)
+    const assigneeId = (globalThis.crypto?.randomUUID?.() ?? `assignee_${Date.now()}_${rnd}`)
src/components/ui/collapsible.tsx (1)

7-17: Incorrect Radix subcomponent identifiers; also missing ref forwarding

Use Trigger/Content (not CollapsibleTrigger/CollapsibleContent) and forward refs to preserve Radix behaviors and consistency with other UI wrappers.

-import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
+import * as CollapsiblePrimitive from '@radix-ui/react-collapsible'
+import * as React from 'react'
 
-function Collapsible({ ...props }: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
-  return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
-}
+const Collapsible = React.forwardRef<
+  React.ElementRef<typeof CollapsiblePrimitive.Root>,
+  React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Root>
+>(({ ...props }, ref) => (
+  <CollapsiblePrimitive.Root ref={ref} data-slot="collapsible" {...props} />
+))
+Collapsible.displayName = CollapsiblePrimitive.Root.displayName
 
-function CollapsibleTrigger({
-  ...props
-}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
-  return <CollapsiblePrimitive.CollapsibleTrigger data-slot="collapsible-trigger" {...props} />
-}
+const CollapsibleTrigger = React.forwardRef<
+  React.ElementRef<typeof CollapsiblePrimitive.Trigger>,
+  React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Trigger>
+>(({ ...props }, ref) => (
+  <CollapsiblePrimitive.Trigger ref={ref} data-slot="collapsible-trigger" {...props} />
+))
+CollapsibleTrigger.displayName = CollapsiblePrimitive.Trigger.displayName
 
-function CollapsibleContent({
-  ...props
-}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
-  return <CollapsiblePrimitive.CollapsibleContent data-slot="collapsible-content" {...props} />
-}
+const CollapsibleContent = React.forwardRef<
+  React.ElementRef<typeof CollapsiblePrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof CollapsiblePrimitive.Content>
+>(({ ...props }, ref) => (
+  <CollapsiblePrimitive.Content ref={ref} data-slot="collapsible-content" {...props} />
+))
+CollapsibleContent.displayName = CollapsiblePrimitive.Content.displayName
src/components/ui/tabs.tsx (1)

30-30: Possible non-existent Tailwind utility

focus-visible:outline-hidden isn’t a default Tailwind class (commonly outline-none). Verify it exists in your config; otherwise replace.

-      'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-xs',
+      'ring-offset-background focus-visible:ring-ring data-[state=active]:bg-background data-[state=active]:text-foreground inline-flex items-center justify-center rounded-sm px-3 py-1.5 text-sm font-medium whitespace-nowrap transition-all focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-xs',

Also applies to: 45-45

src/hooks/useSearch.ts (2)

5-11: Inconsistent discriminant/data typing

type allows 'task'/'team' but data is TUser. Either narrow to 'user' or use a proper discriminated union. Minimal fix below.

-export interface SearchResult {
-  type: 'user' | 'task' | 'team'
+export interface SearchResult {
+  type: 'user'
   id: string
   title: string
   subtitle: string
   data: TUser
 }

31-37: Abort in-flight search requests to prevent stale results

Older search calls may resolve after newer ones and overwrite the UI state. Cancel pending requests via an AbortController signal:

-import { useCallback, useEffect, useState } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
@@
+const abortRef = useRef<AbortController | null>(null)
+
  const performSearch = useCallback(async (searchTerm: string) => {
    if (!searchTerm.trim()) {
      setSearchResults([])
      setError(null)
      return
    }
    setIsSearching(true)
    setError(null)
    try {
-      const { data } = await apiClient.get<TUser[]>(
-        `/v1/users?search=${encodeURIComponent(searchTerm)}`,
-      )
+      abortRef.current?.abort()
+      abortRef.current = new AbortController()
+      const { data } = await apiClient.get<TUser[]>(
+        `/v1/users?search=${encodeURIComponent(searchTerm)}`,
+        { signal: abortRef.current.signal },
+      )
      setSearchResults(data)
    } catch (err) {
      if (err.name !== 'CanceledError') setError(err)
    } finally {
      setIsSearching(false)
    }
  }, [apiClient, setSearchResults, setError, setIsSearching])
@@
-  useEffect(() => {
-    performSearch(debouncedQuery)
-  }, [debouncedQuery, performSearch])
+  useEffect(() => {
+    performSearch(debouncedQuery)
+    return () => abortRef.current?.abort()
+  }, [debouncedQuery, performSearch])

If apiClient doesn’t support signal, implement a monotonically increasing request ID and ignore out-of-order responses.

src/components/ui/sheet.tsx (1)

67-70: Fix invalid Tailwind classes on Close button

rounded-xs and focus:outline-hidden aren’t standard Tailwind utilities. Use rounded-sm and focus:outline-none. Also, data-[state=open]:bg-secondary likely never matches on the Close element; remove to avoid confusion.

Apply this diff:

-        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
+        <SheetPrimitive.Close className="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none disabled:pointer-events-none">
src/api/tasks/tasks.api.ts (4)

27-27: Normalize query keys and fix typo

Use a consistent prefix and correct the “deferredTask” typo for predictable cache keys.

Apply this diff:

-    key: ['tasksApi.createTask'],
+    key: ['TasksApi.createTask'],
@@
-    key: ['tasksApi.updateTask'],
+    key: ['TasksApi.updateTask'],
@@
-    key: ['tasksApi.addTaskToWatchList'],
+    key: ['TasksApi.addTaskToWatchList'],
@@
-    key: ['tasksApi.deferredTask'],
+    key: ['TasksApi.deferTask'],
@@
-    key: ['tasksApi.toggleTaskWatchListStatus'],
+    key: ['TasksApi.toggleTaskWatchListStatus'],

Also applies to: 35-35, 51-51, 58-58, 65-65


51-54: Return type/generic mismatch in addTaskToWatchList

Function returns Promise<void> but the request is typed as returning TWatchListTask. Drop the generic and remove the unused TWatchListTask import.

Apply this diff:

-    fn: async ({ taskId }: AddTaskToWatchListDto): Promise<void> => {
-      await apiClient.post<TWatchListTask>(`/v1/watchlist/tasks`, { taskId })
+    fn: async ({ taskId }: AddTaskToWatchListDto): Promise<void> => {
+      await apiClient.post(`/v1/watchlist/tasks`, { taskId })

And update the imports:

   UpdateTaskDto,
-  TWatchListTask,

19-19: Avoid JSON.stringify in query key

Using JSON.stringify(params) can create unstable keys if property order varies. Prefer a stable object key with shallow primitives, e.g., ['TasksApi.getTasks', params ?? {}], or a dedicated key factory.


28-31: Align updateTask response shape
In src/api/tasks/tasks.api.ts, update updateTask to return Promise<TApiResponse<TTask>> and use apiClient.patch<TApiResponse<TTask>> for consistency with createTask.

src/mocks/data/watchlist.mock.ts (1)

225-231: Mock toggles the wrong field

toggleWatchlistStatus sets isAcknowledged, but the API uses { isActive }. This will desync UI behavior under mocks.

Apply this diff (and ensure TWatchListTask + data include isActive):

-  toggleWatchlistStatus: async (taskId: string, isActive: boolean): Promise<void> => {
+  toggleWatchlistStatus: async (taskId: string, isActive: boolean): Promise<void> => {
     await sleep()
     const taskIndex = mockWatchlistTasks.findIndex((task) => task.taskId === taskId)
     if (taskIndex !== -1) {
-      mockWatchlistTasks[taskIndex].isAcknowledged = isActive
+      mockWatchlistTasks[taskIndex].isActive = isActive
     }
   },
src/config/app-config.ts (1)

31-33: z.treeifyError is not a Zod API; log formatted issues instead.

This will throw at runtime unless you’ve extended Zod. Prefer built-ins.

Apply:

-    console.error('App config error:', z.treeifyError(result.error).properties)
+    console.error('App config error:', result.error.format())

Alternatively: console.error('App config error:', result.error.issues).

.github/workflows/build-and-test.yml (1)

40-44: Don’t hardcode the backend URL; set env once for all steps.

Hardcoding ties the pipeline to “staging” and only for build. Propagate via job-level env and repo/org Variables; also make mocking explicit.

Apply:

 jobs:
   build-and-test:
+    env:
+      # Prefer a repo/org Variable named VITE_BACKEND_API_URL
+      VITE_BACKEND_API_URL: ${{ vars.VITE_BACKEND_API_URL }}
+      VITE_API_MOCKING: 'false'
     runs-on: ubuntu-latest
@@
-      - name: Build:App
-        run: pnpm build
-        env:
-          VITE_BACKEND_API_URL: 'https://services.realdevsquad.com/staging-todo'
+      - name: Build:App
+        run: pnpm build

Optional: add a type-check step before build.

+      - name: Typecheck
+        run: pnpm typecheck
src/components/ui/alert-dialog.tsx (1)

102-119: Optional: forward refs and set displayName for better DX.

Forwarding refs on Action/Cancel/Trigger/Content improves interoperability with wrappers and DevTools. Can be a follow-up.

src/mocks/handlers/tasks.handler.ts (2)

91-101: POST /watchlist/tasks now ignores payload; can’t identify which task to add

The handler no longer reads a body and calls MockWatchlistAPI.addTaskToWatchlist() with no arguments, while the mock implementation doesn’t mutate state. This breaks end-to-end expectations for “Add to watchlist” flows and drifts from typical REST contracts requiring a taskId.

Either (A) restore payload handling, or (B) clearly deprecate the route in favor of PATCH /watchlist/tasks/:taskId and remove the POST entirely.

Option A (preferred to maintain contract parity):

-  http.post(getApiUrl('/watchlist/tasks'), async () => {
+  http.post(getApiUrl('/watchlist/tasks'), async ({ request }) => {
     try {
-      await MockWatchlistAPI.addTaskToWatchlist()
+      const body = (await request.json()) as { taskId: string }
+      if (!body?.taskId) return new HttpResponse(null, { status: 400 })
+      await MockWatchlistAPI.addTaskToWatchlist(body.taskId)
       return new HttpResponse(null, { status: 201 })
     } catch (error) {
       return HttpResponse.json(
         { message: 'Failed to add task to watchlist', error: error },
         { status: 500 },
       )
     }
   }),

If you choose A, update src/mocks/data/watchlist.mock.ts to accept and persist the task:

-  addTaskToWatchlist: async (): Promise<void> => {
-    await sleep()
-  },
+  addTaskToWatchlist: async (taskId: string): Promise<void> => {
+    await sleep()
+    if (!mockWatchlistTasks.find(t => t.taskId === taskId)) {
+      mockWatchlistTasks.push({ taskId, isAcknowledged: false, /* fill other fields as needed */ })
+    }
+  },

Verification:

#!/bin/bash
rg -nP --type=ts --type=tsx -C2 "addTaskToWatchlist\(|/watchlist/tasks(?!/:)"

I can update the mock data structure if you share the minimal fields to create a new watchlist entry.


68-77: Validate task reassignment inputs in mock handler
In src/mocks/handlers/tasks.handler.ts:68-77, change the handler signature to async({ params, request }), parse request.json() and verify params.task_id and body.executor_id, returning a 400 on missing/invalid inputs before returning 200 to keep parity with the real API.

src/components/ui/dropdown-menu.tsx (7)

34-36: Broken Tailwind arbitrary values: use var() inside brackets

max-h-(--radix-...) and origin-(--radix-...) won’t compile. Use bracketed arbitrary values with var(...).

Apply:

-          'bg-popover ... z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden ...',
+          'bg-popover ... z-50 max-h-[var(--radix-dropdown-menu-content-available-height)] min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-x-hidden ...',

62-66: Nonexistent utility outline-hidden

Tailwind doesn’t provide outline-hidden. Likely intended outline-none. Present across multiple components.

-        "focus:bg-accent ... text-sm outline-hidden select-none data-[disabled]:pointer-events-none ..."
+        "focus:bg-accent ... text-sm outline-none select-none data-[disabled]:pointer-events-none ..."

Repeat for CheckboxItem (Line 80), RadioItem (Line 111), and SubTrigger (Line 183).


70-94: Checkbox item: same outline-hidden issue; rest is solid

Replace outline-hidden with outline-none. Otherwise OK.

-      className={cn(
-        "focus:bg-accent ... text-sm outline-hidden select-none ...",
+      className={cn(
+        "focus:bg-accent ... text-sm outline-none select-none ...",

108-114: Radio item: fix outline utility

Same as above.

-      className={cn(
-        "focus:bg-accent ... text-sm outline-hidden select-none ...",
+      className={cn(
+        "focus:bg-accent ... text-sm outline-none select-none ...",

183-186: SubTrigger: fix outline utility

Replace outline-hidden with outline-none.

-        'focus:bg-accent ... text-sm outline-hidden select-none data-[inset]:pl-8',
+        'focus:bg-accent ... text-sm outline-none select-none data-[inset]:pl-8',

202-204: Broken Tailwind arbitrary value in SubContent

Same origin-(--radix-...) issue as Content.

-        'bg-popover ... z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg',
+        'bg-popover ... z-50 min-w-[8rem] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-hidden rounded-md border p-1 shadow-lg',

23-41: Consider forwardRef for Radix primitives

Radix components commonly forward refs. Wrapping without forwardRef can break ref usage in consumers.

Example for Content:

-function DropdownMenuContent({ className, sideOffset = 4, ...props }: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
-  return (
+const DropdownMenuContent = React.forwardRef<
+  React.ElementRef<typeof DropdownMenuPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
+>(function DropdownMenuContent({ className, sideOffset = 4, ...props }, ref) {
+  return (
     <DropdownMenuPrimitive.Portal>
       <DropdownMenuPrimitive.Content
+        ref={ref}
         data-slot="dropdown-menu-content"
         sideOffset={sideOffset}
         className={cn(
           /* classes */
         )}
         {...props}
       />
     </DropdownMenuPrimitive.Portal>
-  )
-}
+  )
+})

Apply similarly for Item, Label, RadioItem, SubTrigger, SubContent.

eslint.config.mjs (1)

27-31: Load plugin where you enforce its rules

You enable react-hooks/exhaustive-deps here but the plugin is only brought in via the later extends. Co-locate the rule with the plugin to avoid rule-resolution issues.

-    rules: {
-      'import-x/no-dynamic-require': 'warn',
-      'import-x/no-nodejs-modules': 'warn',
-      'react-hooks/exhaustive-deps': 'error',
-    },
+    rules: {
+      'import-x/no-dynamic-require': 'warn',
+      'import-x/no-nodejs-modules': 'warn',
+    },

And append to the block starting at Line 34:

   {
     extends: compat.extends(
       'plugin:react/recommended',
       'plugin:react-hooks/recommended',
       'plugin:@typescript-eslint/recommended',
     ),
     settings: {
       react: {
         version: 'detect',
       },
     },
+    rules: {
+      'react-hooks/exhaustive-deps': 'error',
+    },
   },
src/lib/todo-util.ts (1)

9-11: Avoid mutating form state with in-place sort()

todoFormData.labels?.sort() mutates the original array, potentially breaking form state. Clone before sorting.

-    const sortedTodoFormDataLabelIds = todoFormData.labels?.sort()
+    const sortedTodoFormDataLabelIds = todoFormData.labels
+      ? [...todoFormData.labels].sort()
+      : undefined
src/components/todos/create-todo-button.tsx (1)

45-57: Guard against missing assignee in payload

If value.assignee is optional, this will throw at runtime. Spread a conditional object instead.

Apply:

-      onSubmit={(value) =>
-        createTaskMutation.mutate({
-          title: value.title,
-          description: value.description,
-          priority: value.priority,
-          status: value.status,
-          dueAt: value.dueDate,
-          labels: value.labels,
-          assignee_id: value.assignee.value,
-          user_type: value.assignee.type,
-          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
-        })
-      }
+      onSubmit={(value) => {
+        const assignee =
+          value.assignee != null
+            ? { assignee_id: value.assignee.value, user_type: value.assignee.type }
+            : {}
+        createTaskMutation.mutate({
+          title: value.title,
+          description: value.description,
+          priority: value.priority,
+          status: value.status,
+          dueAt: value.dueDate,
+          labels: value.labels,
+          timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+          ...assignee,
+        })
+      }}
src/components/todos/edit-task-button.tsx (1)

26-46: Fix response handling: use res.data consistently

create handler uses res.data, but update uses res.assignee. Likely a mismatch that breaks team-specific invalidation.

Apply:

-      void queryClient.invalidateQueries({ queryKey: TasksApi.getWatchListTasks.key })
+      void queryClient.invalidateQueries({ queryKey: TasksApi.getWatchListTasks.key() })
@@
-      if (res.assignee?.user_type === USER_TYPE_ENUM.TEAM) {
+      if (res.data?.assignee?.user_type === USER_TYPE_ENUM.TEAM) {
         void queryClient.invalidateQueries({
-          queryKey: TasksApi.getTasks.key({ teamId: res.assignee.assignee_id }),
+          queryKey: TasksApi.getTasks.key({ teamId: res.data.assignee.assignee_id }),
         })
       }
src/mocks/data/teams.mock.ts (2)

249-253: Normalize poc_id consistently when members are excluded

Mirror the normalization you do in the includeMembers path.

   if (!includeMembers) {
     return {
       ...team,
+      poc_id: team.poc_id === null ? undefined : team.poc_id,
       users: null,
     }
   }

270-275: Use slice instead of deprecated substr and avoid flaky IDs in tests

substr is legacy; also consider determinism for test stability.

-      id: `team_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
+      id: `team_${Date.now()}_${Math.random().toString(36).slice(2, 11)}`,

Optional: swap Date.now()/Math.random() with a monotonic counter for deterministic mocks.

src/components/teams/add-members-button.tsx (3)

63-66: Fix modal state: onOpenChange handler immediately closes the dialog.

Passing a zero-arg handleCloseModal to onOpenChange will close on open and can prevent the dialog from staying open. Accept the boolean and sync state; clear selection only on close. Also update call sites.

-  const addMembersMutation = useMutation({
+  const addMembersMutation = useMutation({
     mutationFn: TeamsApi.addMembers.fn,
     onSuccess: () => {
-      queryClient.invalidateQueries({
+      queryClient.invalidateQueries({
         queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
       })
-      queryClient.invalidateQueries({ queryKey: TeamsApi.getTeamById.key({ teamId }) })
+      queryClient.invalidateQueries({ queryKey: TeamsApi.getTeamById.key({ teamId }) })
 
       toast.success('Member added successfully!')
-      handleCloseModal()
+      handleOpenChange(false)
     },
     onError: () => {
       toast.error('Failed to add members')
     },
   })
 
-  const handleCloseModal = () => {
-    setSelectedUsers([])
-    setIsAddMembersModalOpen(false)
-  }
+  const handleOpenChange = (open: boolean) => {
+    if (!open) setSelectedUsers([])
+    setIsAddMembersModalOpen(open)
+  }
 
   return (
     <>
       <Button size="sm" variant={variant} onClick={() => setIsAddMembersModalOpen(true)}>
         Add members
       </Button>
 
-      <AlertDialog open={isAddMembersModalOpen} onOpenChange={handleCloseModal}>
+      <AlertDialog open={isAddMembersModalOpen} onOpenChange={handleOpenChange}>
         <AlertDialogContent
           className="w-96 !max-w-sm"
           style={{ maxWidth: '384px', width: '384px' }}
         >
@@
             <div className="flex gap-2">
               <Button
                 variant="outline"
                 className="flex-1"
-                onClick={handleCloseModal}
+                onClick={() => handleOpenChange(false)}
                 disabled={addMembersMutation.isPending}
               >
                 Cancel
               </Button>

Also applies to: 74-75, 93-99, 33-41


75-78: Remove redundant inline width styles.

Tailwind already sets the same widths; drop inline style and the ! override.

-        <AlertDialogContent
-          className="w-96 !max-w-sm"
-          style={{ maxWidth: '384px', width: '384px' }}
-        >
+        <AlertDialogContent className="w-96 max-w-sm">

33-38: Optional: avoid unhandled promises from invalidateQueries.

Prefix with void to signal intentional fire-and-forget.

-      queryClient.invalidateQueries({
+      void queryClient.invalidateQueries({
         queryKey: TeamsApi.getTeamById.key({ teamId, member: true }),
       })
-      queryClient.invalidateQueries({ queryKey: TeamsApi.getTeamById.key({ teamId }) })
+      void queryClient.invalidateQueries({ queryKey: TeamsApi.getTeamById.key({ teamId }) })
src/modules/dashboard/components/dashboard-watchlist-table.tsx (2)

7-18: Return a stable empty list from select to avoid undefined downstream.

Simplifies consumers like TodoListTable and avoids conditional rendering bugs.

Apply:

-  select: (data) =>
-    data.tasks?.map((task) => ({
+  select: (data) =>
+    (data.tasks ?? []).map((task) => ({
       ...task,
       id: task.taskId,
       in_watchlist: true,
       labels: task.labels,
       priority: task.priority,
     })),

24-24: Pass a default array to TodoListTable.

Prevents passing undefined when there are no tasks.

-  return <TodoListTable showActions isLoading={isLoading} tasks={data} />
+  return <TodoListTable showActions isLoading={isLoading} tasks={data ?? []} />
src/components/users/reassign-user.tsx (2)

48-50: Surface error details in toast (helps debugging).

Include the error message from the mutation.

-    onError: () => {
-      toast.error('Failed to reassign task, please try again')
-    },
+    onError: (err: unknown) => {
+      const msg = err instanceof Error ? err.message : 'Unknown error'
+      toast.error(`Failed to reassign task: ${msg}`)
+    },

104-111: Add accessible label to the icon-only button.

Improves screen reader UX; tooltip alone isn’t sufficient.

-          <Button
+          <Button
             size="icon"
             variant="ghost"
             onClick={() => setShowModal(true)}
             className="hover:bg-gray-200 hover:text-gray-800 active:bg-gray-300 active:text-gray-900"
+            aria-label="Reassign user"
           >
src/components/users/user-and-team-search.tsx (3)

69-76: TypeScript excess property errors: remove undeclared searchValue.

TUserOrTeamOption lacks searchValue; object literals will fail excess property checks.

Either remove it:

   const userOptions: TUserOrTeamOption[] =
     usersList?.map((user) => ({
       label: user.name,
       value: user.id,
       type: 'user',
-      searchValue: user.name,
     })) ?? []

   const teamOptions: TUserOrTeamOption[] = filteredTeams.map((team) => ({
     label: team.name,
     value: team.id,
     type: 'team',
-    searchValue: team.name,
   }))

Or extend the type if needed:

 type TUserOrTeamOption = {
   label: string
   value: string
   type: 'user' | 'team'
+  searchValue?: string
 }

Also applies to: 77-83


112-123: Avoid effect churn: remove options from deps.

options is recomputed every render and isn’t used inside the effect; it can trigger unnecessary re-runs.

-}, [value, selectedOption, options])
+}, [value, selectedOption])

156-159: Copy: reflect users and teams in empty state.

Minor UX polish.

-            <CommandEmpty>{isDataLoading ? 'Loading...' : 'No users found.'}</CommandEmpty>
+            <CommandEmpty>{isDataLoading ? 'Loading...' : 'No results found.'}</CommandEmpty>
src/components/users/user-profile-menu.tsx (2)

21-36: Prefer DropdownMenuItem asChild to avoid extra wrapper semantics.

Use asChild to delegate focus/ARIA to the Button and avoid nested interactive roles.

-    <DropdownMenuItem className="!bg-transparent p-0">
+    <DropdownMenuItem asChild className="!bg-transparent p-0">
       <Button
         variant="ghost"
         onClick={handleLogout}
         disabled={logoutMutation.isPending}
         className="w-full justify-start text-red-500 hover:bg-red-100 hover:text-red-600 disabled:text-gray-700"
       >
         {logoutMutation.isPending ? (
           <Loader2 className="h-4 w-4 animate-spin" />
         ) : (
           <LogOutIcon className="h-4 w-4 text-inherit" />
         )}
         Logout
       </Button>
-    </DropdownMenuItem>
+    </DropdownMenuItem>

55-57: Guard against undefined user.name; add alt text.

Accessing user.name.slice(...) will throw if name is undefined/null. Provide robust fallbacks and set alt.

-          <AvatarImage src={user.picture} />
-          <AvatarFallback>{user.name.slice(0, 2).toUpperCase()}</AvatarFallback>
+          <AvatarImage
+            src={user.picture}
+            alt={`${(user.name ?? user.username ?? 'User')} avatar`}
+          />
+          <AvatarFallback>
+            {(user.name ?? user.username ?? user.email ?? 'U').slice(0, 2).toUpperCase()}
+          </AvatarFallback>
src/main.tsx (1)

1-24: Add root ESLint config (.eslintrc.json) with TypeScript/Node import resolvers
Create a .eslintrc.json at the project root configuring import/resolver and import-x/resolver for typescript and node to fix no-unresolved errors on @tanstack/react-router and react-dom/client.

src/components/ui/tooltip.tsx (1)

19-25: Avoid wrapping each Tooltip with its own Provider

Wrapping TooltipPrimitive.Root with TooltipProvider inside Tooltip creates a provider per tooltip instance. Prefer mounting a single TooltipProvider once near the app root and letting Tooltip just render Root. This reduces extra providers and centralizes delayDuration control.

Apply:

-function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
-  return (
-    <TooltipProvider>
-      <TooltipPrimitive.Root data-slot="tooltip" {...props} />
-    </TooltipProvider>
-  )
-}
+function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
+  return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
+}
src/components/layout/page-header.tsx (1)

12-31: Consider deriving title from route meta instead of sidebar config.

TanStack Router supports route meta/handle; using it avoids drift between routes and sidebar.

src/api/teams/teams.api.ts (1)

12-29: Keep strong typing for API registry (optional).

If TApiMethodsRecord was removed, consider a lightweight constraint to catch drift:

-export const TeamsApi = {
+export const TeamsApi = {
   getTeams: {
@@
   },
 } 
+as const

Or define a local ApiMethod type and satisfies it to maintain structure checks.

src/components/layout/app-sidebar.tsx (1)

129-140: Active-link detection via startsWith is brittle on path variants.

Use router-aware matching to avoid false positives/negatives on trailing slashes or param segments.

Example:

// Instead of pathname.startsWith(item.baseUrl)
<Link to={item.url} activeOptions={{ exact: false }}>
  {item.title}
</Link>

Or compute isActive via useMatch({ to: item.baseUrl, fuzzy: true }).

src/components/users/signin-button.tsx (2)

95-99: AlertDialogTrigger asChild with non-interactive root.

With the above change, AnimatedButton’s root is Button, making it a proper trigger target. This fixes click/focus inconsistencies.


22-22: Typo in SVG attribute breaks icon rendering.

height="24ps" should be 24px.

-      height="24ps"
+      height="24px"
src/components/todos/watchlist-button.tsx (1)

68-73: Icon-only buttons need accessible labels.

Add aria-labels for screen readers.

-          <Button
+          <Button
             variant="ghost"
             size="icon"
             className="hover:bg-gray-200 hover:text-gray-800 active:bg-gray-300 active:text-gray-900"
+            aria-label="Remove task from watchlist"
             onClick={() => toggleWatchListStatusMutation.mutate({ taskId, isActive: false })}
           >
...
-        <Button
+        <Button
           variant="ghost"
           size="icon"
           className="hover:bg-gray-200 hover:text-gray-800 active:bg-gray-300 active:text-gray-900"
+          aria-label="Add task to watchlist"
           onClick={handleAddTaskToWatchlist}
         >

Also applies to: 89-94

src/modules/admin/invite-codes-table.tsx (1)

26-33: Add accessible label to copy button.

Improve SR support.

-          <Button
+          <Button
             variant="ghost"
             size="sm"
             onClick={() => copyToClipboard(inviteCode.code)}
             className="h-6 w-6 p-0"
+            aria-label="Copy invite code"
           >
src/mocks/handlers/teams.handler.ts (1)

69-95: Retain teamId param for timeline route.

Even with static data, keep param parsing to surface invalid URLs early.

-  http.get(getApiUrl('/teams/:teamId/activity-timeline'), async () => {
+  http.get(getApiUrl('/teams/:teamId/activity-timeline'), async ({ params }) => {
     try {
+      const { teamId } = params
+      if (!teamId) return new HttpResponse(null, { status: 400 })
       const activities = {
         timeline: [
src/components/todos/todo-list-table.tsx (1)

220-224: Control the Searchbar value from URL state.

Using defaultValue desyncs on back/forward navigation. Bind value to search.

-          defaultValue={search.search}
+          value={search.search ?? ''}
src/modules/admin/index.tsx (1)

103-107: Use TabsContent for accessibility instead of manual show/hide.

Shadcn’s Tabs provides TabsContent with proper roles/ARIA. Replace the custom div toggling.

Add to imports:

-import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs'
+import { Tabs, TabsList, TabsTrigger, TabsContent } from '@/components/ui/tabs'

Replace content mapping:

-        {tabs.map((tab) => (
-          <div key={tab.id} className={activeTab === tab.id ? 'block' : 'hidden'}>
-            {tab.content}
-          </div>
-        ))}
+        {tabs.map((tab) => (
+          <TabsContent key={tab.id} value={tab.id}>
+            {tab.content}
+          </TabsContent>
+        ))}

Also applies to: 1-1

♻️ Duplicate comments (3)
src/components/layout/app-sidebar.tsx (1)

22-22: Resolve missing modules '@tanstack/react-router' and 'lucide-react'.

ESLint flags unresolved imports. Confirm both are installed and versions align.

See dependency check script in src/components/layout/page-header.tsx (Line 2).

src/components/users/signin-button.tsx (1)

4-4: Resolve '@tanstack/react-router' import resolution.

See dependency check script in src/components/layout/page-header.tsx (Line 2).

src/modules/dashboard/components/dashboard-deferred-table.tsx (1)

6-8: Resolve TanStack imports.

ESLint reports unresolved '@tanstack/react-query' and '@tanstack/react-router'. Verify deps installed.

See dependency check script in src/components/layout/page-header.tsx (Line 2).

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.

Review continued from previous batch...

@MayankBansal12 MayankBansal12 force-pushed the anuj/migrate-to-tanstack branch from 6612ac2 to 954ceb1 Compare September 6, 2025 15:37
MayankBansal12
MayankBansal12 previously approved these changes Sep 6, 2025
Copy link
Member

@MayankBansal12 MayankBansal12 left a comment

Choose a reason for hiding this comment

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

Have tested all the changes locally for all cases

Hariom01010
Hariom01010 previously approved these changes Sep 6, 2025
@MayankBansal12 MayankBansal12 changed the title feat(#196): Migration to Tanstack Router from Next JS file-based routing feat(#196): Migration to Tanstack Router from Next JS Sep 7, 2025
Copy link

@Achintya-Chatterjee Achintya-Chatterjee left a comment

Choose a reason for hiding this comment

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

tested the changes locally, and it's working fine, hence approving this PR

@MayankBansal12 MayankBansal12 self-assigned this Sep 11, 2025
@yesyash
Copy link
Contributor

yesyash commented Sep 11, 2025

@AnujChhikara @Achintya-Chatterjee @Hariom01010 @MayankBansal12 have we tested the scenario in which if the user is not logged in and they to access any internal routes they're redirected to the login page?

If yes, can you please point me to the code blocks which enables this behavior?

@yesyash
Copy link
Contributor

yesyash commented Sep 11, 2025

@AnujChhikara do we need the _internal.teams.tsx route?

@AnujChhikara AnujChhikara changed the title feat(#196): Migration to Tanstack Router from Next JS breaking-change(#196): Migration to Tanstack Router from Next JS Sep 11, 2025
@yesyash yesyash changed the title breaking-change(#196): Migration to Tanstack Router from Next JS BREAKING CHANGE(#196): Migration to Tanstack Router from Next JS Sep 11, 2025
@yesyash yesyash merged commit ce7e497 into develop Sep 11, 2025
3 checks passed
@yesyash yesyash deleted the anuj/migrate-to-tanstack branch September 11, 2025 12:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature task A big ticket item that needs to come up as a feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants