Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion packages/shared/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,14 @@
"test:coverage": "vitest run --coverage --silent passed-only"
},
"dependencies": {
"@fontsource-variable/inter": "5.3.0",
"@fortawesome/fontawesome-svg-core": "7.3.1",
"@fortawesome/free-regular-svg-icons": "7.3.1",
"@fortawesome/free-solid-svg-icons": "7.3.1",
"@fortawesome/react-fontawesome": "3.5.0",
"@fontsource-variable/inter": "5.3.0",
"class-variance-authority": "0.7.1",
"clsx": "2.1.1",
"cn": "0.2.6",
"next-themes": "0.4.6",
"radix-ui": "1.6.7",
"react-syntax-highlighter": "16.1.1",
Expand Down
60 changes: 60 additions & 0 deletions packages/shared/src/components/shadcn/accordion.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import * as React from 'react';
import { cn } from 'cn';
import { Accordion as AccordionPrimitive } from 'radix-ui';
import { ChevronDownIcon, ChevronUpIcon } from 'lucide-react';

function Accordion({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Root>) {
return <AccordionPrimitive.Root data-slot="accordion" className={cn('flex w-full flex-col', className)} {...props} />;
}

function AccordionItem({ className, ...props }: React.ComponentProps<typeof AccordionPrimitive.Item>) {
return (
<AccordionPrimitive.Item data-slot="accordion-item" className={cn('not-last:border-b', className)} {...props} />
);
}

function AccordionTrigger({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Trigger>) {
return (
<AccordionPrimitive.Header className="flex">
<AccordionPrimitive.Trigger
data-slot="accordion-trigger"
className={cn(
'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',
className,
)}
{...props}
>
{children}
<ChevronDownIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none shrink-0 group-aria-expanded/accordion-trigger:hidden"
/>
<ChevronUpIcon
data-slot="accordion-trigger-icon"
className="pointer-events-none hidden shrink-0 group-aria-expanded/accordion-trigger:inline"
/>
</AccordionPrimitive.Trigger>
</AccordionPrimitive.Header>
);
}

function AccordionContent({ className, children, ...props }: React.ComponentProps<typeof AccordionPrimitive.Content>) {
return (
<AccordionPrimitive.Content
data-slot="accordion-content"
className="data-open:animate-accordion-down data-closed:animate-accordion-up overflow-hidden text-sm"
{...props}
>
<div
className={cn(
'[&_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',
className,
)}
>
{children}
</div>
</AccordionPrimitive.Content>
);
}

export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
24 changes: 0 additions & 24 deletions packages/shared/src/utils/css.test.ts

This file was deleted.

23 changes: 2 additions & 21 deletions packages/shared/src/utils/css.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,2 @@
/**
* @module css
* @description Utility functions for handling CSS class names, particularly in the context of Tailwind CSS.
* This module provides a function to conditionally construct `className` strings and merge Tailwind CSS classes without conflicts.
* It leverages the `clsx` library for conditional class name construction and the `tailwind-merge` library for merging Tailwind CSS classes.
* @see {@link https://github.com/lukeed/clsx} for `clsx` documentation.
* @see {@link https://github.com/dcastil/tailwind-merge} for `tailwind-merge` documentation.
*/

import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';

/**
* Conditionally construct `className` strings. Merge Tailwind CSS classes
* without conflicts.
* @param inputs - Class values used to calculate the `className` value.
* @returns {string} - A consolidated string containing CSS class names.
*/
export const cn = (...inputs: ClassValue[]): string => {
return twMerge(clsx(inputs));
};
// Backwards compatibility for the `cn` utility function for shadcn.
export { cn } from 'cn';
7 changes: 6 additions & 1 deletion packages/shared/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ export default mergeConfig(
include: ['src/**/*.test.ts', 'src/**/*.test.tsx'],
coverage: {
include: ['src/**/*.ts', 'src/**/*.tsx'],
exclude: ['src/components/shadcn/**', ...coverageConfigDefaults.exclude],
exclude: [
'src/components/shadcn/**',
'src/utils/css.ts',
'src/hooks/use-mobile.ts',
...coverageConfigDefaults.exclude,
],
},
},
}),
Expand Down
11 changes: 8 additions & 3 deletions packages/web/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

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

import ErrorBoundary from '@/common/components/Errors/ErrorBoundary';
import ErrorFallback from '@/common/components/Errors/ErrorFallback';
Expand All @@ -29,7 +30,7 @@ const queryClient = new QueryClient({
* The application. The outermost component of the React application
* hierarchy. Declares application-wide providers.
*/
export const App = () => {
const App = () => {
return (
<div id="app" className="bg-background text-foreground min-h-screen" data-testid="app">
<ErrorBoundary fallback={<ErrorFallback />}>
Expand All @@ -38,8 +39,10 @@ export const App = () => {
<Theme />
<AuthContextProvider>
<AxiosContextProvider>
<RouterProvider router={router} />
<Toaster />
<TooltipProvider>
<RouterProvider router={router} />
<Toaster />
</TooltipProvider>
</AxiosContextProvider>
</AuthContextProvider>
<ReactQueryDevtools initialIsOpen={false} />
Expand All @@ -49,3 +52,5 @@ export const App = () => {
</div>
);
};

export { App };
2 changes: 1 addition & 1 deletion packages/web/src/__fixtures__/todos.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Task } from '@/pages/Tasks/api/useGetUserTasks';
import type { Task } from '@/common/types/task';

export const todosFixture: Task[] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ vi.mock('react-i18next', async () => {
const original = await vi.importActual('react-i18next');
return {
...original,
useTranslation: () => ({ i18n: { changeLanguage: mockChangeLanguage } }),
useTranslation: () => ({ i18n: { changeLanguage: mockChangeLanguage }, t: vi.fn() }),
};
});

Expand Down
32 changes: 22 additions & 10 deletions packages/web/src/common/components/Button/LanguageToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import {
DropdownMenuItem,
} from '@react-starter/shared/components/shadcn/dropdown-menu';
import { Button } from '@react-starter/shared/components/shadcn/button';
import { Tooltip, TooltipContent, TooltipTrigger } from '@react-starter/shared/components/shadcn/tooltip';

/**
* The `LanguageToggle` component renders a `Dropdown` which allows users
* to select the language in which they wish to view the application.
*/
export const LanguageToggle = () => {
const { i18n } = useTranslation();
const { i18n, t } = useTranslation();

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

return (
<DropdownMenu data-testid="dropdown-language">
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" title="Select Language" data-testid="button-language-menu-trigger">
<Languages aria-label="Select Language" data-testid="icon-language" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-32">
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>
<Button
variant="outline"
size="icon"
className="cursor-pointer"
aria-label={t('select-language')}
data-testid="button-language-menu-trigger"
>
<Languages data-testid="icon-language" />
</Button>
</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent>{t('select-language')}</TooltipContent>
</Tooltip>
<DropdownMenuContent align="end" className="w-32" onCloseAutoFocus={(e) => e.preventDefault()}>
<DropdownMenuGroup>
<DropdownMenuLabel>Languages</DropdownMenuLabel>
<DropdownMenuItem onClick={() => setLanguage('en')} data-testid="dropdown-item-en">
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('en')} data-testid="dropdown-item-en">
English
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage('fr')} data-testid="dropdown-item-fr">
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('fr')} data-testid="dropdown-item-fr">
French
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setLanguage('es')} data-testid="dropdown-item-es">
<DropdownMenuItem className="cursor-pointer" onClick={() => setLanguage('es')} data-testid="dropdown-item-es">
Spanish
</DropdownMenuItem>
</DropdownMenuGroup>
Expand Down
63 changes: 35 additions & 28 deletions packages/web/src/common/components/Button/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,51 @@
import { Moon, Sun } from 'lucide-react';
import { useTranslation } from 'react-i18next';

import { cn } from '@react-starter/shared/utils/css';
import { Tooltip, TooltipContent, TooltipTrigger } from '@react-starter/shared/components/shadcn/tooltip';
import { Button } from '@react-starter/shared/components/shadcn/button';

import { PropsWithClassName } from '@react-starter/shared/types/components';
import { useSetSettings } from '@/common/api/useSetSettings';
import { useSettings } from '@/common/hooks/useSettings';
import { Button } from '@react-starter/shared/components/shadcn/button';

/**
* The `ThemeToggle` React component renders a `Button` which allows users
* to toggle between light and dark themes.
* @param {PropsWithClassName} [props] - Component properties, `PropsWithClassName`.
*/
export const ThemeToggle = ({ className }: PropsWithClassName) => {
export const ThemeToggle = ({ className }: Pick<React.ComponentProps<typeof Button>, 'className'>) => {
const { t } = useTranslation();
const settings = useSettings();
const { mutate: setSettings } = useSetSettings();

return (
<>
{settings?.theme === 'light' ? (
<Button
variant="outline"
size="icon"
className={className}
title="Dark Mode"
onClick={() => setSettings({ theme: 'dark' })}
data-testid="button-theme-dark"
>
<Moon data-testid="icon-dark-mode" />
</Button>
) : (
<Button
variant="outline"
size="icon"
className={className}
title="Light Mode"
onClick={() => setSettings({ theme: 'light' })}
data-testid="button-theme-light"
>
<Sun data-testid="icon-light-mode" />
</Button>
)}
</>
<Tooltip>
<TooltipTrigger asChild>
{settings?.theme === 'light' ? (
<Button
variant="outline"
size="icon"
className={cn('cursor-pointer', className)}
aria-label={t('theme.switch-dark')}
onClick={() => setSettings({ theme: 'dark' })}
data-testid="button-theme-dark"
>
<Moon data-testid="icon-dark-mode" />
</Button>
) : (
<Button
variant="outline"
size="icon"
className={cn('cursor-pointer', className)}
aria-label={t('theme.switch-light')}
onClick={() => setSettings({ theme: 'light' })}
data-testid="button-theme-light"
>
<Sun data-testid="icon-light-mode" />
</Button>
)}
</TooltipTrigger>
<TooltipContent>{settings?.theme === 'light' ? t('theme.switch-dark') : t('theme.switch-light')}</TooltipContent>
</Tooltip>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ describe('AppSidebar', () => {

// ASSERT
expect(screen.getByTestId('app-sidebar')).toBeDefined();
expect(screen.getByText('Sign Out')).toBeDefined();
expect(screen.getByText(/Sign out/i)).toBeDefined();
expect(screen.getByText('Settings')).toBeDefined();
expect(screen.getByText('Tasks')).toBeDefined();
});
Expand Down
Loading