BREAKING CHANGE(#196): Migration to Tanstack Router from Next JS#197
BREAKING CHANGE(#196): Migration to Tanstack Router from Next JS#197
Conversation
…ion and task management
…lude /dist/ directory
…stency ,wrap up work for today 🌙 good night
…cies, update README for project structure and local setup
|
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
|
|
Important Review skippedAuto incremental reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the You can disable this status message by setting the Summary by CodeRabbit
WalkthroughMigrates 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
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
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
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
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Possibly related issues
Possibly related PRs
Suggested labels
Suggested reviewers
Poem
✨ Finishing touches🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
I have done regression testing of the migration on call with @AnujChhikara and all the functionalities are working as expected. |
There was a problem hiding this comment.
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/$teamIdexactly matches the generated route ID insrc/routes/_internal.teams.$teamId.tsxandrouteTree.gen.ts, so no update is needed.- Optional: in
src/components/teams/team-activity.tsx, addenabled: !!teamIdto youruseQuerycall to prevent it running whenteamIdis 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
searchstate unless you need it elsewhere.If you don’t need the current
searchelsewhere:- 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
satisfiesremoves compile-time checks on the API surface. Also,['AuthApi.logout']is inferred asstring[], 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
forwardRefprevents 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,
datamay degrade toanyand 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 oneObject.values-based key was found insrc/api/users/users.api.ts. Replace...Object.values(params||{})with a deterministic serialization (e.g. sorted key–value pairs orJSON.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 forwardingUse
Trigger/Content(notCollapsibleTrigger/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.displayNamesrc/components/ui/tabs.tsx (1)
30-30: Possible non-existent Tailwind utility
focus-visible:outline-hiddenisn’t a default Tailwind class (commonlyoutline-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
typeallows 'task'/'team' butdataisTUser. 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 resultsOlder 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
apiClientdoesn’t supportsignal, 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-xsandfocus:outline-hiddenaren’t standard Tailwind utilities. Userounded-smandfocus:outline-none. Also,data-[state=open]:bg-secondarylikely 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 typoUse 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 addTaskToWatchListFunction returns
Promise<void>but the request is typed as returningTWatchListTask. Drop the generic and remove the unusedTWatchListTaskimport.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 keyUsing
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, updateupdateTaskto returnPromise<TApiResponse<TTask>>and useapiClient.patch<TApiResponse<TTask>>for consistency withcreateTask.src/mocks/data/watchlist.mock.ts (1)
225-231: Mock toggles the wrong field
toggleWatchlistStatussetsisAcknowledged, but the API uses{ isActive }. This will desync UI behavior under mocks.Apply this diff (and ensure
TWatchListTask+ data includeisActive):- 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.treeifyErroris 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 buildOptional: add a type-check step before build.
+ - name: Typecheck + run: pnpm typechecksrc/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 addThe 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 ataskId.Either (A) restore payload handling, or (B) clearly deprecate the route in favor of
PATCH /watchlist/tasks/:taskIdand 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.tsto 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
Insrc/mocks/handlers/tasks.handler.ts:68-77, change the handler signature toasync({ params, request }), parserequest.json()and verifyparams.task_idandbody.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: usevar()inside brackets
max-h-(--radix-...)andorigin-(--radix-...)won’t compile. Use bracketed arbitrary values withvar(...).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 utilityoutline-hiddenTailwind doesn’t provide
outline-hidden. Likely intendedoutline-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: sameoutline-hiddenissue; rest is solidReplace
outline-hiddenwithoutline-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 utilitySame 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 utilityReplace
outline-hiddenwithoutline-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 SubContentSame
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 primitivesRadix components commonly forward refs. Wrapping without
forwardRefcan 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 rulesYou enable
react-hooks/exhaustive-depshere but the plugin is only brought in via the laterextends. 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-placesort()
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() + : undefinedsrc/components/todos/create-todo-button.tsx (1)
45-57: Guard against missing assignee in payloadIf
value.assigneeis 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: useres.dataconsistently
createhandler usesres.data, butupdateusesres.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: Normalizepoc_idconsistently when members are excludedMirror the normalization you do in the
includeMemberspath.if (!includeMembers) { return { ...team, + poc_id: team.poc_id === null ? undefined : team.poc_id, users: null, } }
270-275: Usesliceinstead of deprecatedsubstrand avoid flaky IDs in tests
substris 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
handleCloseModaltoonOpenChangewill 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
voidto 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.jsonat the project root configuringimport/resolverandimport-x/resolverfortypescriptandnodeto fixno-unresolvederrors on@tanstack/react-routerandreact-dom/client.src/components/ui/tooltip.tsx (1)
19-25: Avoid wrapping each Tooltip with its own ProviderWrapping
TooltipPrimitive.RootwithTooltipProviderinsideTooltipcreates a provider per tooltip instance. Prefer mounting a singleTooltipProvideronce near the app root and lettingTooltipjust renderRoot. This reduces extra providers and centralizesdelayDurationcontrol.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
TApiMethodsRecordwas removed, consider a lightweight constraint to catch drift:-export const TeamsApi = { +export const TeamsApi = { getTeams: { @@ }, } +as constOr define a local ApiMethod type and
satisfiesit 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
isActiveviauseMatch({ 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 be24px.- 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: RetainteamIdparam 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
defaultValuedesyncs on back/forward navigation. Bindvalueto 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
TabsContentwith proper roles/ARIA. Replace the customdivtoggling.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).
…ling across components
6612ac2 to
954ceb1
Compare
Achintya-Chatterjee
left a comment
There was a problem hiding this comment.
tested the changes locally, and it's working fine, hence approving this PR
|
@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? |
|
@AnujChhikara do we need the |
f517bb9
Date: 5 Sep 2025
Developer Name: @AnujChhikara
Issue Ticket Number
Description
Documentation Updated?
Under Feature Flag
Database Changes
Breaking Changes
Development Tested?
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.