Skip to content

Commit 80ffdaf

Browse files
authored
Task list improvements (#195)
* chore: bump cn to 0.2.6 and remove duplicate @fontsource-variable/inter entry * feat: add Accordion component with trigger and content functionality * feat: integrate TooltipProvider into App and test wrappers * feat: TaskList improvements * test: improve error handling in TaskDeleteDialog tests and update mock response status * fix: add aria-label to DropdownMenuTrigger for accessibility * feat: add Tooltip to LanguageToggle * feat: add Tooltip to LanguageToggle and ThemeToggle * feat: implement i18n support for AppSidebar and AppSidebarTrigger components * feat: add sign-in button to Header component with translation support * feat: add cursor pointer style to interactive components for better UX * feat: refactor TaskDeleteDialog to use trigger prop and navigate on success * refactor: move mock setup for react-router-dom outside of describe block in TaskEdit tests * chore: shadcn migrate cn * test: AddTaskButton unit tests * feat: enhance task update feedback with localized messages
1 parent 6d1b100 commit 80ffdaf

55 files changed

Lines changed: 778 additions & 477 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎package-lock.json‎

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/shared/package.json‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,14 @@
1616
"test:coverage": "vitest run --coverage --silent passed-only"
1717
},
1818
"dependencies": {
19+
"@fontsource-variable/inter": "5.3.0",
1920
"@fortawesome/fontawesome-svg-core": "7.3.1",
2021
"@fortawesome/free-regular-svg-icons": "7.3.1",
2122
"@fortawesome/free-solid-svg-icons": "7.3.1",
2223
"@fortawesome/react-fontawesome": "3.5.0",
23-
"@fontsource-variable/inter": "5.3.0",
2424
"class-variance-authority": "0.7.1",
2525
"clsx": "2.1.1",
26+
"cn": "0.2.6",
2627
"next-themes": "0.4.6",
2728
"radix-ui": "1.6.7",
2829
"react-syntax-highlighter": "16.1.1",
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import * as React from 'react';
2+
import { cn } from 'cn';
3+
import { Accordion as AccordionPrimitive } from 'radix-ui';
4+
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';
5+
6+
function Accordion({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
7+
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />;
8+
}
9+
10+
function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
11+
return (
12+
<AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
13+
);
14+
}
15+
16+
function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
17+
return (
18+
<AccordionPrimitive.Header className="flex">
19+
<AccordionPrimitive.Trigger
20+
data-slot="accordion-trigger"
21+
className={cn(
22+
'group/accordion-trigger focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:after:border-ring **:data-[slot=accordion-trigger-icon]:text-muted-foreground relative flex flex-1 items-start justify-between rounded-lg border border-transparent py-2.5 text-left text-sm font-medium transition-all outline-none hover:underline focus-visible:ring-3 disabled:pointer-events-none disabled:opacity-50 **:data-[slot=accordion-trigger-icon]:ml-auto **:data-[slot=accordion-trigger-icon]:size-4',
23+
className,
24+
)}
25+
{...props}
26+
>
27+
{children}
28+
<ChevronDownIcon
29+
data-slot="accordion-trigger-icon"
30+
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
31+
/>
32+
<ChevronUpIcon
33+
data-slot="accordion-trigger-icon"
34+
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
35+
/>
36+
</AccordionPrimitive.Trigger>
37+
</AccordionPrimitive.Header>
38+
);
39+
}
40+
41+
function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
42+
return (
43+
<AccordionPrimitive.Content
44+
data-slot="accordion-content"
45+
className="data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm"
46+
{...props}
47+
>
48+
<div
49+
className={cn(
50+
'[&_a]:hover:text-foreground h-(--radix-accordion-content-height) pt-0 pb-2.5 [&_a]:underline [&_a]:underline-offset-3 [&_p:not(:last-child)]:mb-4',
51+
className,
52+
)}
53+
>
54+
{children}
55+
</div>
56+
</AccordionPrimitive.Content>
57+
);
58+
}
59+
60+
export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };

‎packages/shared/src/utils/css.test.ts‎

Lines changed: 0 additions & 24 deletions
This file was deleted.

‎packages/shared/src/utils/css.ts‎

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,2 @@
1-
/**
2-
* @module css
3-
* @description Utility functions for handling CSS class names, particularly in the context of Tailwind CSS.
4-
* This module provides a function to conditionally construct `className` strings and merge Tailwind CSS classes without conflicts.
5-
* It leverages the `clsx` library for conditional class name construction and the `tailwind-merge` library for merging Tailwind CSS classes.
6-
* @see {@link https://github.com/lukeed/clsx} for `clsx` documentation.
7-
* @see {@link https://github.com/dcastil/tailwind-merge} for `tailwind-merge` documentation.
8-
*/
9-
10-
import { clsx, type ClassValue } from 'clsx';
11-
import { twMerge } from 'tailwind-merge';
12-
13-
/**
14-
* Conditionally construct `className` strings. Merge Tailwind CSS classes
15-
* without conflicts.
16-
* @param inputs - Class values used to calculate the `className` value.
17-
* @returns {string} - A consolidated string containing CSS class names.
18-
*/
19-
export const cn = (...inputs: ClassValue[]): string => {
20-
return twMerge(clsx(inputs));
21-
};
1+
// Backwards compatibility for the `cn` utility function for shadcn.
2+
export { cn } from 'cn';

‎packages/shared/vitest.config.ts‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,12 @@ export default mergeConfig(
2525
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
2626
coverage: {
2727
include: ['src/**/*.ts', 'src/**/*.tsx'],
28-
exclude: ['src/components/shadcn/**', ...coverageConfigDefaults.exclude],
28+
exclude: [
29+
'src/components/shadcn/**',
30+
'src/utils/css.ts',
31+
'src/hooks/use-mobile.ts',
32+
...coverageConfigDefaults.exclude,
33+
],
2934
},
3035
},
3136
}),

‎packages/web/src/App.tsx‎

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
33
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
44

55
import { Toaster } from '@react-starter/shared/components/shadcn/sonner';
6+
import { TooltipProvider } from '@react-starter/shared/components/shadcn/tooltip';
67

78
import ErrorBoundary from '@/common/components/Errors/ErrorBoundary';
89
import ErrorFallback from '@/common/components/Errors/ErrorFallback';
@@ -29,7 +30,7 @@ const queryClient = new QueryClient({
2930
* The application. The outermost component of the React application
3031
* hierarchy. Declares application-wide providers.
3132
*/
32-
export const App = () => {
33+
const App = () => {
3334
return (
3435
<div id="app" className="bg-background text-foreground min-h-screen" data-testid="app">
3536
<ErrorBoundary fallback={<ErrorFallback />}>
@@ -38,8 +39,10 @@ export const App = () => {
3839
<Theme />
3940
<AuthContextProvider>
4041
<AxiosContextProvider>
41-
<RouterProvider router={router} />
42-
<Toaster />
42+
<TooltipProvider>
43+
<RouterProvider router={router} />
44+
<Toaster />
45+
</TooltipProvider>
4346
</AxiosContextProvider>
4447
</AuthContextProvider>
4548
<ReactQueryDevtools initialIsOpen={false} />
@@ -49,3 +52,5 @@ export const App = () => {
4952
</div>
5053
);
5154
};
55+
56+
export { App };

‎packages/web/src/__fixtures__/todos.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Task } from '@/pages/Tasks/api/useGetUserTasks';
1+
import type { Task } from '@/common/types/task';
22

33
export const todosFixture: Task[] = [
44
{

‎packages/web/src/common/components/Button/LanguageToggle.test.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ vi.mock('react-i18next', async () => {
1313
const original = await vi.importActual('react-i18next');
1414
return {
1515
...original,
16-
useTranslation: () => ({ i18n: { changeLanguage: mockChangeLanguage } }),
16+
useTranslation: () => ({ i18n: { changeLanguage: mockChangeLanguage }, t: vi.fn() }),
1717
};
1818
});
1919

‎packages/web/src/common/components/Button/LanguageToggle.tsx‎

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,14 @@ import {
1212
DropdownMenuItem,
1313
} from '@react-starter/shared/components/shadcn/dropdown-menu';
1414
import { Button } from '@react-starter/shared/components/shadcn/button';
15+
import { Tooltip, TooltipContent, TooltipTrigger } from '@react-starter/shared/components/shadcn/tooltip';
1516

1617
/**
1718
* The `LanguageToggle` component renders a `Dropdown` which allows users
1819
* to select the language in which they wish to view the application.
1920
*/
2021
export const LanguageToggle = () => {
21-
const { i18n } = useTranslation();
22+
const { i18n, t } = useTranslation();
2223

2324
/**
2425
* Set the application-wide langague code used for i18n.
@@ -31,21 +32,32 @@ export const LanguageToggle = () => {
3132

3233
return (
3334
<DropdownMenu data-testid="dropdown-language">
34-
<DropdownMenuTrigger asChild>
35-
<Button variant="outline" size="icon" title="Select Language" data-testid="button-language-menu-trigger">
36-
<Languages aria-label="Select Language" data-testid="icon-language" />
37-
</Button>
38-
</DropdownMenuTrigger>
39-
<DropdownMenuContent align="end" className="w-32">
35+
<Tooltip>
36+
<TooltipTrigger asChild>
37+
<DropdownMenuTrigger asChild>
38+
<Button
39+
variant="outline"
40+
size="icon"
41+
className="cursor-pointer"
42+
aria-label={t('select-language')}
43+
data-testid="button-language-menu-trigger"
44+
>
45+
<Languages data-testid="icon-language" />
46+
</Button>
47+
</DropdownMenuTrigger>
48+
</TooltipTrigger>
49+
<TooltipContent>{t('select-language')}</TooltipContent>
50+
</Tooltip>
51+
<DropdownMenuContent align="end" className="w-32" onCloseAutoFocus={(e) => e.preventDefault()}>
4052
<DropdownMenuGroup>
4153
<DropdownMenuLabel>Languages</DropdownMenuLabel>
42-
<DropdownMenuItem onClick={() => setLanguage('en')} data-testid="dropdown-item-en">
54+
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('en')} data-testid="dropdown-item-en">
4355
English
4456
</DropdownMenuItem>
45-
<DropdownMenuItem onClick={() => setLanguage('fr')} data-testid="dropdown-item-fr">
57+
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('fr')} data-testid="dropdown-item-fr">
4658
French
4759
</DropdownMenuItem>
48-
<DropdownMenuItem onClick={() => setLanguage('es')} data-testid="dropdown-item-es">
60+
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('es')} data-testid="dropdown-item-es">
4961
Spanish
5062
</DropdownMenuItem>
5163
</DropdownMenuGroup>

0 commit comments

Comments
 (0)