This commit is contained in:
David Arranz 2024-06-09 22:04:46 +02:00
parent cdb6e7de99
commit a2995234ee
50 changed files with 1087 additions and 432 deletions

View File

@ -44,6 +44,8 @@
"class-variance-authority": "^0.7.0",
"cmdk": "^1.0.0",
"date-fns": "^3.6.0",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"joi": "^17.13.1",
"lucide-react": "^0.379.0",
"react": "^18.2.0",
@ -51,6 +53,7 @@
"react-day-picker": "^8.10.1",
"react-dom": "^18.2.0",
"react-hook-form": "^7.51.5",
"react-i18next": "^14.1.2",
"react-resizable-panels": "^2.0.19",
"react-router-dom": "^6.23.1",
"react-secure-storage": "^1.3.2",

View File

@ -1,23 +1,28 @@
import { RouterProvider, createBrowserRouter } from "react-router-dom";
import { LoginPage, LogoutPage, StartPage } from "./app";
import { CatalogList } from "./app/catalog";
import { DashboardPage } from "./app/dashboard";
import { ProtectedRoute } from "./components";
import { DashboardPage, LogoutPage } from "./pages";
import { LoginPage } from "./pages/LoginPage";
export const Routes = () => {
// Define public routes accessible to all users
const routesForPublic = [
{
path: "/service",
element: <div>Service Page</div>,
},
{
path: "/about-us",
element: <div>About Us</div>,
path: "/",
Component: StartPage,
},
];
// Define routes accessible only to authenticated users
const routesForAuthenticatedOnly = [
{
path: "/home",
Component: DashboardPage,
},
{
path: "/catalog",
Component: CatalogList,
},
{
path: "/profile",
element: (
@ -48,10 +53,6 @@ export const Routes = () => {
// Define routes accessible only to non-authenticated users
const routesForNotAuthenticatedOnly = [
{
path: "/",
Component: DashboardPage,
},
{
path: "/login",
Component: LoginPage,
@ -61,8 +62,8 @@ export const Routes = () => {
// Combine and conditionally include routes based on authentication status
const router = createBrowserRouter([
...routesForPublic,
...routesForNotAuthenticatedOnly,
...routesForAuthenticatedOnly,
...routesForNotAuthenticatedOnly,
]);
// Provide the router configuration using RouterProvider

View File

@ -19,12 +19,14 @@ import Joi from "joi";
import { AlertCircleIcon } from "lucide-react";
import { useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import { Trans, useTranslation } from "react-i18next";
import { Link } from "react-router-dom";
import SpanishJoiMessages from "../spanish-joi-messages.json";
type LoginDataForm = ILogin_DTO;
export const LoginPage = () => {
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const { mutate: login } = useLogin({
onSuccess: (data) => {
@ -55,7 +57,6 @@ export const LoginPage = () => {
});
const onSubmit: SubmitHandler<LoginDataForm> = async (data) => {
console.log(data);
try {
setLoading(true);
login({ email: data.email, password: data.password });
@ -74,8 +75,12 @@ export const LoginPage = () => {
<Card>
<CardHeader>
<UeckoLogo className='inline-block m-auto mb-6 align-middle max-w-32' />
<CardTitle>Presupuestador para distribuidores</CardTitle>
<CardDescription>Enter your email below to login to your account</CardDescription>
<CardTitle>
<Trans i18nKey='login_page.title' />
</CardTitle>
<CardDescription>
<Trans i18nKey='login_page.description' />
</CardDescription>
</CardHeader>
<CardContent>
<Form {...form}>
@ -84,9 +89,9 @@ export const LoginPage = () => {
<div className='grid gap-6'>
<FormTextField
disabled={loading}
label='Email'
label={t("login_page.email_label")}
type='email'
placeholder='micorreo@ejemplo.com'
placeholder={t("login_page.email_placeholder")}
{...form.register("email", {
required: true,
})}
@ -96,7 +101,7 @@ export const LoginPage = () => {
<div className='grid gap-6'>
<FormTextField
disabled={loading}
label='Contraseña'
label={t("login_page.password_label")}
type='password'
{...form.register("password", {
required: true,
@ -104,10 +109,10 @@ export const LoginPage = () => {
errors={form.formState.errors}
/>
<div className='mb-4 -mt-2 text-sm'>
¿Has olvidado tu contraseña?
<Trans i18nKey='login_page.forgotten_password' />
<br />
<Link to='https://uecko.com/distribuidores' className='underline'>
Contacta con nosotros
<Trans i18nKey='login_page.contact_us' />
</Link>
</div>
</div>
@ -115,20 +120,22 @@ export const LoginPage = () => {
{form.formState.errors.root?.message && (
<Alert variant='destructive'>
<AlertCircleIcon className='w-4 h-4' />
<AlertTitle>Heads up!</AlertTitle>
<AlertTitle>
<Trans i18nKey='login_page.error' />
</AlertTitle>
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
</Alert>
)}
<Button disabled={loading} type='submit' className='w-full'>
Entrar
<Trans i18nKey='login_page.login' />
</Button>
<div className='mt-4 text-sm text-center'>
¿Quieres ser distribuidor de Uecko?
<Trans i18nKey='login_page.become_dealer' />
<br />
<Link to='https://uecko.com/distribuidores' className='underline'>
Contacta con nosotros
<Trans i18nKey='login_page.contact_us' />
</Link>
</div>
</div>

View File

@ -0,0 +1,25 @@
import { LoadingOverlay } from "@/components";
import { useIsLoggedIn } from "@/lib/hooks";
import { Navigate } from "react-router-dom";
export const StartPage = () => {
const { status, data: { authenticated, redirectTo } = {} } = useIsLoggedIn();
if (status !== "success") {
return <LoadingOverlay />;
}
if (!authenticated) {
return (
<Navigate
to={redirectTo ?? "/login"}
replace
state={{
error: "No authentication, please complete the login process.",
}}
/>
);
}
return <Navigate to={"/home"} replace />;
};

View File

@ -1,11 +0,0 @@
import { createAxiosDataProvider } from "@/lib/axios";
import { Outlet } from "react-router-dom";
export const AuthLayout = (): JSX.Element => {
const authDataProvider = createAxiosDataProvider(import.meta.env.VITE_API_URL);
return (
<AuthProvider dataProvider={authDataProvider}>
<Outlet />
</AuthProvider>
);
};

View File

@ -1 +0,0 @@
export * from "./AuthLayout";

View File

@ -0,0 +1 @@
export * from "./list";

View File

@ -0,0 +1,312 @@
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@/ui";
import { File, ListFilter, MoreHorizontal, PlusCircle } from "lucide-react";
import { Layout, LayoutContent, LayoutHeader } from "@/components";
export const CatalogList = ({ children }) => {
return (
<Layout>
<LayoutHeader />
<LayoutContent>
<Tabs defaultValue='all'>
<div className='flex items-center'>
<TabsList>
<TabsTrigger value='all'>All</TabsTrigger>
<TabsTrigger value='active'>Active</TabsTrigger>
<TabsTrigger value='draft'>Draft</TabsTrigger>
<TabsTrigger value='archived' className='hidden sm:flex'>
Archived
</TabsTrigger>
</TabsList>
<div className='flex items-center gap-2 ml-auto'>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='outline' size='sm' className='h-8 gap-1'>
<ListFilter className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Filter</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem checked>Active</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
<Button size='sm' variant='outline' className='h-8 gap-1'>
<File className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Export</span>
</Button>
<Button size='sm' className='h-8 gap-1'>
<PlusCircle className='h-3.5 w-3.5' />
<span className='sr-only sm:not-sr-only sm:whitespace-nowrap'>Add Product</span>
</Button>
</div>
</div>
<TabsContent value='all'>
<Card x-chunk='dashboard-06-chunk-0'>
<CardHeader>
<CardTitle>Products</CardTitle>
<CardDescription>
Manage your products and view their sales performance.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead className='hidden w-[100px] sm:table-cell'>
<span className='sr-only'>Image</span>
</TableHead>
<TableHead>Name</TableHead>
<TableHead>Status</TableHead>
<TableHead className='hidden md:table-cell'>Price</TableHead>
<TableHead className='hidden md:table-cell'>Total Sales</TableHead>
<TableHead className='hidden md:table-cell'>Created at</TableHead>
<TableHead>
<span className='sr-only'>Actions</span>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Laser Lemonade Machine</TableCell>
<TableCell>
<Badge variant='outline'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$499.99</TableCell>
<TableCell className='hidden md:table-cell'>25</TableCell>
<TableCell className='hidden md:table-cell'>2023-07-12 10:42 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Hypernova Headphones</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$129.99</TableCell>
<TableCell className='hidden md:table-cell'>100</TableCell>
<TableCell className='hidden md:table-cell'>2023-10-18 03:21 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>AeroGlow Desk Lamp</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$39.99</TableCell>
<TableCell className='hidden md:table-cell'>50</TableCell>
<TableCell className='hidden md:table-cell'>2023-11-29 08:15 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>TechTonic Energy Drink</TableCell>
<TableCell>
<Badge variant='secondary'>Draft</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$2.99</TableCell>
<TableCell className='hidden md:table-cell'>0</TableCell>
<TableCell className='hidden md:table-cell'>2023-12-25 11:59 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Gamer Gear Pro Controller</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$59.99</TableCell>
<TableCell className='hidden md:table-cell'>75</TableCell>
<TableCell className='hidden md:table-cell'>2024-01-01 12:00 AM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
<TableRow>
<TableCell className='hidden sm:table-cell'>
<img
alt='Product image'
className='object-cover rounded-md aspect-square'
height='64'
src='/placeholder.svg'
width='64'
/>
</TableCell>
<TableCell className='font-medium'>Luminous VR Headset</TableCell>
<TableCell>
<Badge variant='outline'>Active</Badge>
</TableCell>
<TableCell className='hidden md:table-cell'>$199.99</TableCell>
<TableCell className='hidden md:table-cell'>30</TableCell>
<TableCell className='hidden md:table-cell'>2024-02-14 02:14 PM</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button aria-haspopup='true' size='icon' variant='ghost'>
<MoreHorizontal className='w-4 h-4' />
<span className='sr-only'>Toggle menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end'>
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem>Edit</DropdownMenuItem>
<DropdownMenuItem>Delete</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
</TableBody>
</Table>
</CardContent>
<CardFooter>
<div className='text-xs text-muted-foreground'>
Showing <strong>1-10</strong> of <strong>32</strong> products
</div>
</CardFooter>
</Card>
</TabsContent>
</Tabs>
</LayoutContent>
</Layout>
);
};

View File

@ -0,0 +1,96 @@
import { Layout, LayoutHeader } from "@/components";
import {
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Input,
} from "@/ui";
import { Checkbox } from "@radix-ui/react-checkbox";
import { SyntheticEvent, useState } from "react";
import { Link } from "react-router-dom";
export const DashboardPage = () => {
const [logoutAlertVisible, setLogoutAlertVisible] = useState<boolean>(false);
const openLogoutAlert = (event: Event) => {
event.preventDefault();
setLogoutAlertVisible(true);
return;
};
const closeLogoutAlert = (event: SyntheticEvent) => {
event.preventDefault();
setLogoutAlertVisible(false);
return;
};
return (
<Layout>
<LayoutHeader />
<main className='flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10'>
<div className='grid w-full max-w-6xl gap-2 mx-auto'>
<h1 className='text-3xl font-semibold'>Settings</h1>
</div>
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
<nav className='grid gap-4 text-sm text-muted-foreground' x-chunk='dashboard-04-chunk-0'>
<Link to='#' className='font-semibold text-primary'>
General
</Link>
<Link to='#'>Security</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card x-chunk='dashboard-04-chunk-1'>
<CardHeader>
<CardTitle>Store Name</CardTitle>
<CardDescription>Used to identify your store in the marketplace.</CardDescription>
</CardHeader>
<CardContent>
<form>
<Input placeholder='Store Name' />
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-04-chunk-2'>
<CardHeader>
<CardTitle>Plugins Directory</CardTitle>
<CardDescription>
The directory within your project, in which your plugins are located.
</CardDescription>
</CardHeader>
<CardContent>
<form className='flex flex-col gap-4'>
<Input placeholder='Project Name' defaultValue='/content/plugins' />
<div className='flex items-center space-x-2'>
<Checkbox id='include' defaultChecked />
<label
htmlFor='include'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Allow administrators to change the directory.
</label>
</div>
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
</div>
</div>
</main>
</Layout>
);
};

View File

@ -0,0 +1 @@
export * from "./list";

8
client/src/app/index.ts Normal file
View File

@ -0,0 +1,8 @@
export * from "./ErrorPage";
export * from "./LoginPage";
export * from "./LogoutPage";
export * from "./StartPage";
export * from "./catalog";
export * from "./dashboard";
//export * from "./dealers";
//export * from "./quotes";

View File

@ -0,0 +1 @@
export * from "./list";

View File

@ -0,0 +1,55 @@
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/ui";
import { SyntheticEvent } from "react";
import { Link } from "react-router-dom";
interface CustomDialogProps {
isOpen: boolean;
onCancel: (event: SyntheticEvent) => void;
onConfirm: (event: SyntheticEvent) => void;
title: React.ReactNode;
description: React.ReactNode;
cancelLabel: React.ReactNode;
confirmLabel: React.ReactNode;
}
export const CustomDialog = ({
isOpen,
onCancel: onClose,
onConfirm,
title,
description,
cancelLabel,
confirmLabel,
}: CustomDialogProps) => {
return (
<AlertDialog open={isOpen}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>{title}</AlertDialogTitle>
<AlertDialogDescription>{description}</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Link to='#' onClick={onClose}>
{cancelLabel}
</Link>
</AlertDialogCancel>
<AlertDialogAction>
<Link to='#' onClick={onConfirm}>
{confirmLabel}
</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
);
};

View File

@ -0,0 +1 @@
export * from "./CustomDialog";

View File

@ -0,0 +1,7 @@
import { PropsWithChildren } from "react";
export const Layout = ({ children }: PropsWithChildren) => {
return <div className='flex flex-col w-full min-h-screen'>{children}</div>;
};
Layout.displayName = "Layout";

View File

@ -0,0 +1,11 @@
import { PropsWithChildren } from "react";
export const LayoutContent = ({ children }: PropsWithChildren) => {
return (
<main className='flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10'>
{children}
</main>
);
};
LayoutContent.displayName = "LayoutContent";

View File

@ -0,0 +1,91 @@
import { Button, Input, Sheet, SheetContent, SheetTrigger } from "@/ui";
import { t } from "i18next";
import { MenuIcon, Package2Icon, SearchIcon } from "lucide-react";
import { Trans } from "react-i18next";
import { Link } from "react-router-dom";
import { UeckoLogo } from "../UeckoLogo";
import { UserButton } from "./components/UserButton";
export const LayoutHeader = () => {
return (
<header className='sticky top-0 flex items-center h-16 gap-8 px-4 border-b bg-background md:px-6'>
<nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
<Link to='/' className='flex items-center font-semibold'>
<UeckoLogo className='w-24' />
<span className='sr-only'>Uecko</span>
</Link>
<Link to='/home' className='transition-colors text-muted-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.home' />
</Link>
<Link
to='/quotes'
className='transition-colors text-muted-foreground hover:text-foreground'
>
<Trans i18nKey='main_menu.quotes' />
</Link>
<Link
to='/catalog'
className='transition-colors text-muted-foreground hover:text-foreground'
>
<Trans i18nKey='main_menu.catalog' />
</Link>
<Link
to='/dealers'
className='transition-colors text-muted-foreground hover:text-foreground'
>
<Trans i18nKey='main_menu.dealers' />
</Link>
<Link to='/settings' className='transition-colors text-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.settings' />
</Link>
</nav>
<Sheet>
<SheetTrigger asChild>
<Button variant='outline' size='icon' className='shrink-0 md:hidden'>
<MenuIcon className='w-5 h-5' />
<span className='sr-only'>Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side='left'>
<nav className='grid gap-6 text-lg font-medium'>
<Link to='/' className='flex items-center gap-2 text-lg font-semibold'>
<Package2Icon className='w-6 h-6' />
<span className='sr-only'>Uecko</span>
</Link>
<Link to='/home' className='text-muted-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.home' />
</Link>
<Link to='/quotes' className='text-muted-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.quotes' />
</Link>
<Link to='/catalog' className='text-muted-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.catalog' />
</Link>
<Link to='/dealers' className='text-muted-foreground hover:text-foreground'>
<Trans i18nKey='main_menu.dealers' />
</Link>
<Link to='/settings' className='hover:text-foreground'>
<Trans i18nKey='main_menu.settings' />
</Link>
</nav>
</SheetContent>
</Sheet>
<div className='flex items-center w-full gap-4 md:ml-auto md:gap-2 lg:gap-4'>
<form className='flex-1 ml-auto'>
<div className='relative'>
<SearchIcon className='absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground' />
<Input
type='search'
placeholder={t("main_menu.search_placeholder")}
className='pl-8 sm:w-[300px] md:w-[200px] lg:w-[350px] xl:w-[550px] 2xl:w-[750px]'
/>
</div>
</form>
<UserButton />
</div>
</header>
);
};
LayoutHeader.displayName = "LayoutHeader";

View File

@ -0,0 +1,20 @@
import { Avatar, AvatarFallback, AvatarImage } from "@/ui";
import { CircleUserIcon } from "lucide-react";
import { PropsWithChildren } from "react";
export const UserAvatar = ({
name = "",
src,
}: PropsWithChildren & {
name?: string;
src?: string;
}) => {
return (
<Avatar>
<AvatarImage src={src} alt={name} />
<AvatarFallback>
<CircleUserIcon className='w-5 h-5' />
</AvatarFallback>
</Avatar>
);
};

View File

@ -0,0 +1,87 @@
import { useCustomDialog } from "@/lib/hooks";
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
} from "@/ui";
import { t } from "i18next";
import {
CircleUserIcon,
LogOutIcon,
MessageCircleQuestionIcon,
SettingsIcon,
UserIcon,
} from "lucide-react";
import { SyntheticEvent, useState } from "react";
import { useNavigate } from "react-router-dom";
export const UserButton = () => {
const [userMenuOpened, setUserMenuOpened] = useState(false);
const navigate = useNavigate();
const { openDialog: openLogoutDialog, DialogComponent: LogoutDialog } = useCustomDialog({
title: "Salir de la cuenta",
description: "¿Desea salir de su cuenta?",
confirmLabel: t("main_menu.user.logout"),
onConfirm: () => {
navigate("/logout");
},
});
//const { data, status } = useGetIdentity();
const openUserMenu = (event: SyntheticEvent) => {
event.preventDefault();
setUserMenuOpened(true);
};
/*const closeUserMenu = (event: SyntheticEvent) => {
event.preventDefault();
setUserMenuOpened(false);
};*/
/*if (status !== "success") {
return <></>;
}*/
return (
<DropdownMenu open={userMenuOpened} onOpenChange={setUserMenuOpened}>
<DropdownMenuTrigger asChild>
<Button variant='secondary' size='icon' className='rounded-full' onClick={openUserMenu}>
<CircleUserIcon className='w-5 h-5' />
<span className='sr-only'>{t("main_menu.user.user_menu")}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-56'>
<DropdownMenuLabel>{t("main_menu.user.my_account")}</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserIcon className='w-4 h-4 mr-2' />
<span>{t("main_menu.user.profile")}</span>
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
<SettingsIcon className='w-4 h-4 mr-2' />
<span>{t("main_menu.user.settings")}</span>
</DropdownMenuItem>
<DropdownMenuItem>
<MessageCircleQuestionIcon className='w-4 h-4 mr-2' />
<span>{t("main_menu.user.support")}</span>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={() => openLogoutDialog()}>
<LogOutIcon className='w-4 h-4 mr-2' />
<span>{t("main_menu.user.logout")}</span>
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
{LogoutDialog}
</DropdownMenu>
);
};

View File

@ -0,0 +1,3 @@
export * from "./Layout";
export * from "./LayoutContent";
export * from "./LayoutHeader";

View File

@ -0,0 +1 @@
export * from "./UeckoLogo";

View File

@ -1,5 +1,7 @@
export * from "./Container";
export * from "./CustomDialog";
export * from "./Forms";
export * from "./Layout";
export * from "./LoadingIndicator";
export * from "./LoadingOverlay";
export * from "./ProtectedRoute";

View File

@ -2,51 +2,51 @@
@tailwind components;
@tailwind utilities;
/* https://ui.jln.dev/ Light Blue */
/* https://ui.jln.dev/ Shivering Harpy Brown */
@layer base {
:root {
--background: 210 40% 96.08%;
--foreground: 334 55% 1%;
--muted: 214.29 31.82% 91.37%;
--muted-foreground: 334 9% 37%;
--popover: 334 62% 100%;
--popover-foreground: 334 55% 1%;
--card: 334 62% 100%;
--card-foreground: 334 55% 1%;
--border: 334 5% 95%;
--input: 334 5% 95%;
--primary: 209.23 58.21% 39.41%;
--primary-foreground: 0 0% 100%;
--secondary: 213.75 20.25% 69.02%;
--secondary-foreground: 334 0% 100%;
--accent: 214.29 31.82% 91.37%;
--accent-foreground: 334 20% 22%;
--destructive: 348.37 78.4% 49.02%;
--destructive-foreground: 18 0% 100%;
--ring: 209.23 58.21% 39.41%;
--background: 30 65% 100%;
--foreground: 30 75% 0%;
--muted: 30 17% 95%;
--muted-foreground: 30 15% 26%;
--popover: 0 0% 99%;
--popover-foreground: 0 0% 0%;
--card: 0 0% 99%;
--card-foreground: 0 0% 0%;
--border: 30 4% 91%;
--input: 30 4% 91%;
--primary: 30 26% 23%;
--primary-foreground: 30 26% 83%;
--secondary: 30 12% 82%;
--secondary-foreground: 30 12% 22%;
--accent: 30 22% 76%;
--accent-foreground: 30 22% 16%;
--destructive: 18 91% 33%;
--destructive-foreground: 0 0% 100%;
--ring: 30 26% 23%;
--radius: 0.5rem;
}
.dark {
--background: 222.22 47.37% 11.18%;
--foreground: 334 34% 98%;
--muted: 215.38 16.32% 46.86%;
--muted-foreground: 334 0% 87.69%;
--popover: 217.24 32.58% 17.45%;
--popover-foreground: 334 34% 98%;
--card: 217.24 32.58% 17.45%;
--card-foreground: 334 34% 98%;
--border: 334 0% 32.31%;
--input: 215.29 25% 26.67%;
--primary: 227.56 53.78% 49.22%;
--primary-foreground: 0 0% 100%;
--secondary: 214.29 5.04% 27.25%;
--secondary-foreground: 334 0% 100%;
--accent: 222.22 47.37% 11.18%;
--accent-foreground: 226.73 0% 100%;
--destructive: 358.82 84.44% 64.71%;
--destructive-foreground: 0 0% 100%;
--ring: 227.56 53.78% 49.22%;
--background: 30 53% 1%;
--foreground: 30 11% 98%;
--muted: 30 17% 5%;
--muted-foreground: 30 15% 74%;
--popover: 30 53% 2%;
--popover-foreground: 30 11% 99%;
--card: 30 53% 2%;
--card-foreground: 30 11% 99%;
--border: 30 4% 14%;
--input: 30 4% 14%;
--primary: 30 26% 23%;
--primary-foreground: 30 26% 83%;
--secondary: 30 7% 15%;
--secondary-foreground: 30 7% 75%;
--accent: 30 16% 24%;
--accent-foreground: 30 16% 84%;
--destructive: 18 91% 59%;
--destructive-foreground: 0 0% 0%;
--ring: 30 26% 23%;
}
}

View File

@ -42,79 +42,6 @@ export const defaultAxiosRequestConfig = {
/**
* Creates an initial 'axios' instance with custom settings.
*/
const axiosInstance = setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig));
/**
* Handle all responses.
*/
/*axiosInstance.interceptors.response.use(
(response) => {
if (!response) {
return Promise.resolve({
statusCode: 500,
body: null,
});
}
const { data, headers } = response;
const DTOBody = !isArray(data)
? data
: {
items: data,
totalCount: headers["x-total-count"]
? parseInt(headers["x-total-count"])
: data.length,
};
const result = {
statusCode: response.status,
body: DTOBody,
};
//console.log('Axios OK => ', result);
return Promise.resolve(result);
},
(error) => {
console.group("Axios error:");
if (error.response) {
// La respuesta fue hecha y el servidor respondió con un código de estado
// que esta fuera del rango de 2xx
console.log("1 => El servidor respondió con un código de estado > 200");
console.log(error.response.data);
console.log(error.response.status);
} else if (error.request) {
// La petición fue hecha pero no se recibió respuesta
console.log("2 => El servidor no respondió");
console.log(error.request);
} else {
// Algo paso al preparar la petición que lanzo un Error
console.log("3 => Error desconocido");
console.log("Error", error.message);
}
const customError = {
message: error.response?.data?.message,
statusCode: error.response?.status,
};
console.log("Axios BAD => ", error, customError);
console.groupEnd();
return Promise.reject(customError);
}
);*/
/**
* Replaces main `axios` instance with the custom-one.
*
* @param config - Axios configuration object.
* @returns A promise object of a response of the HTTP request with the 'data' object already
* destructured.
*/
/*const axios = <T>(config: AxiosRequestConfig) =>
axiosInstance.request<any, T>(config);
export default axios;*/
export const createAxiosInstance = () => axiosInstance;
export const createAxiosInstance = () =>
setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig));

View File

@ -1,4 +1,4 @@
import { ILogin_DTO, ILogin_Response_DTO } from "@shared/contexts";
import { IIdentity_Response_DTO, ILogin_DTO, ILogin_Response_DTO } from "@shared/contexts";
import secureLocalStorage from "react-secure-storage";
import { IAuthActions } from "../hooks";
import { createAxiosInstance } from "./axiosInstance";
@ -10,7 +10,7 @@ export const createAxiosAuthActions = (
login: async ({ email, password }: ILogin_DTO) => {
// eslint-disable-next-line no-useless-catch
try {
const { data } = await httpClient.request<ILogin_Response_DTO>({
const result = await httpClient.request<ILogin_Response_DTO>({
url: `${apiUrl}/auth/login`,
method: "POST",
data: {
@ -19,7 +19,8 @@ export const createAxiosAuthActions = (
},
});
secureLocalStorage.setItem("uecko", JSON.stringify(data));
const { data } = result;
secureLocalStorage.setItem("uecko.auth", data);
return {
success: true,
@ -36,28 +37,51 @@ export const createAxiosAuthActions = (
};
}
},
logout: () => {
secureLocalStorage.removeItem("uecko");
secureLocalStorage.clear();
return Promise.resolve({
success: true,
redirectTo: "/login",
});
},
check: () => {
const data: ILogin_Response_DTO = JSON.parse(
secureLocalStorage.getItem("uecko")?.toString() || ""
);
const profile = secureLocalStorage.getItem("uecko.auth") as ILogin_Response_DTO;
return Promise.resolve(
data.token
profile?.token
? {
authenticated: true,
}
: { authenticated: false, redirectTo: "/login" }
);
},
getIdentity: async () => {
try {
const result = await httpClient.request<IIdentity_Response_DTO>({
url: `${apiUrl}/auth/identity`,
method: "GET",
});
const { data } = result;
const profile = secureLocalStorage.getItem("uecko.auth") as ILogin_Response_DTO;
if (profile?.id === data?.id) {
secureLocalStorage.setItem("uecko.profile", data);
return data;
}
return undefined;
} catch (error) {
return undefined;
}
},
onError: (error: any) => {
console.error(error);
secureLocalStorage.removeItem("uecko");
secureLocalStorage.clear();
return Promise.resolve({
error,
logout: true,

View File

@ -1,20 +1,14 @@
import {
AxiosError,
AxiosInstance,
AxiosResponse,
InternalAxiosRequestConfig,
} from "axios";
//use(onFulfilled?: ((value: V) => V | Promise<V>) | null,
//onRejected?: ((error: any) => any) | null, options?: AxiosInterceptorOptions): number;
const onRequest = (
request: InternalAxiosRequestConfig
): InternalAxiosRequestConfig => {
/*console.group("[request]");
console.dir(request);
console.groupEnd();*/
import { ILogin_Response_DTO } from "@shared/contexts";
import { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios";
import secureLocalStorage from "react-secure-storage";
const onRequest = (request: InternalAxiosRequestConfig): InternalAxiosRequestConfig => {
const authInfo: ILogin_Response_DTO = secureLocalStorage.getItem(
"uecko.auth"
) as ILogin_Response_DTO;
if (authInfo && authInfo.token && request.headers) {
request.headers.Authorization = `Bearer ${authInfo.token}`;
}
return request;
};
@ -95,9 +89,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
throw error;
};
export function setupInterceptorsTo(
axiosInstance: AxiosInstance
): AxiosInstance {
export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance {
axiosInstance.interceptors.request.use(onRequest, onRequestError);
axiosInstance.interceptors.response.use(onResponse, onResponseError);
return axiosInstance;

View File

@ -0,0 +1,36 @@
export const getNameInitials = (name: string, count = 2) => {
const initials = name
.split(" ")
.map((n) => n[0])
.join("");
const filtered = initials.replace(/[^a-zA-Z]/g, "");
return filtered.slice(0, count).toUpperCase();
};
/*
* generates random colors from https://ant.design/docs/spec/colors. <color-4> used.
*/
export const getRandomColorFromString = (text: string) => {
const colors = [
"#ff9c6e",
"#ff7875",
"#ffc069",
"#ffd666",
"#fadb14",
"#95de64",
"#5cdbd3",
"#69c0ff",
"#85a5ff",
"#b37feb",
"#ff85c0",
];
let hash = 0;
for (let i = 0; i < text.length; i++) {
hash = text.charCodeAt(i) + ((hash << 5) - hash);
hash = hash & hash;
}
hash = ((hash % colors.length) + colors.length) % colors.length;
return colors[hash];
};

View File

@ -12,4 +12,5 @@ export * from "./useUrlId";
*/
export * from "./useAuth";
export * from "./useCustomDialog";
export * from "./useTheme";

View File

@ -1,5 +1,6 @@
export * from "./AuthActions";
export * from "./AuthContext";
export * from "./useAuth";
export * from "./useGetIdentity";
export * from "./useIsLoggedIn";
export * from "./useLogin";

View File

@ -0,0 +1,14 @@
import { useAuth } from "@/lib/hooks";
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
import { useQueryKey } from "../useQueryKey";
export const useGetIdentity = (queryOptions?: UseQueryOptions) => {
const keys = useQueryKey();
const { getIdentity } = useAuth();
return useQuery({
queryKey: keys().auth().action("identity").get(),
queryFn: getIdentity,
...queryOptions,
});
};

View File

@ -1,16 +1,14 @@
import { AuthActionCheckResponse, useAuth } from "@/lib/hooks";
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
import { useAuth } from "@/lib/hooks";
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
import { useQueryKey } from "../useQueryKey";
export const useIsLoggedIn = (
params?: UseMutationOptions<AuthActionCheckResponse, Error, unknown>
) => {
export const useIsLoggedIn = (queryOptions?: UseQueryOptions) => {
const keys = useQueryKey();
const { check } = useAuth();
return useMutation({
mutationKey: keys().auth().action("check").get(),
mutationFn: check,
...params,
return useQuery({
queryKey: keys().auth().action("check").get(),
queryFn: check,
...queryOptions,
});
};

View File

@ -25,6 +25,8 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
},
onError: (error, variables, context) => {
const { message } = error;
console.error(message);
toast.error(message);
if (onError) {

View File

@ -0,0 +1 @@
export * from "./useCustomDialog";

View File

@ -0,0 +1,66 @@
import { CustomDialog } from "@/components/CustomDialog"; // Ajusta la ruta según sea necesario
import { t } from "i18next";
import { ReactNode, SyntheticEvent, useState } from "react";
interface DialogConfig {
title: ReactNode;
description: ReactNode;
cancelLabel: ReactNode;
confirmLabel: ReactNode;
onCancel: (event?: SyntheticEvent) => void;
onConfirm: (event?: SyntheticEvent) => void;
}
export const useCustomDialog = (config?: Partial<DialogConfig>) => {
const [isOpen, setIsOpen] = useState<boolean>(false);
const [dialogConfig, setDialogConfig] = useState<DialogConfig | undefined>({
title: "Título",
description: "Descripción",
cancelLabel: t("common.no"),
confirmLabel: t("common.yes"),
onCancel: () => {},
onConfirm: () => {},
...config,
});
const openDialog = (event?: SyntheticEvent, config?: DialogConfig) => {
event?.preventDefault();
if (config) {
setDialogConfig(config);
}
setIsOpen(true);
};
const handleCancel = (event?: SyntheticEvent) => {
event?.preventDefault();
setIsOpen(false);
dialogConfig?.onCancel(event);
};
const handleConfirm = (event?: SyntheticEvent) => {
event?.preventDefault();
setIsOpen(false);
dialogConfig?.onConfirm(event);
};
const DialogComponent = dialogConfig ? (
<CustomDialog
isOpen={isOpen}
onCancel={handleCancel}
onConfirm={handleConfirm}
title={dialogConfig.title}
description={dialogConfig.description}
cancelLabel={dialogConfig.cancelLabel}
confirmLabel={dialogConfig.confirmLabel}
/>
) : (
<></>
);
return {
openDialog,
cancelDialog: handleCancel,
confirmDialog: handleConfirm,
DialogComponent,
};
};

View File

@ -0,0 +1 @@
export * from "./useUnsavedChangesNotifier";

View File

@ -0,0 +1,16 @@
import React from "react";
import { useCustomDialog } from "../useCustomDialog";
type UnsavedChangesNotifierProps = {
translationKey?: string;
message?: string;
};
export const UnsavedChangesNotifier: React.FC<UnsavedChangesNotifierProps> = () => {
const { openDialog: openWarmDialog, DialogComponent: WarmDialog } = useCustomDialog({
title: "Hay cambios sin guardar",
description: "Are you sure you want to leave? You have unsaved changes.",
});
return <>{WarmDialog}</>;
};

29
client/src/lib/i18n.ts Normal file
View File

@ -0,0 +1,29 @@
import i18n from "i18next";
import LanguageDetector from "i18next-browser-languagedetector";
import { initReactI18next } from "react-i18next";
import catResources from "../locales/ca.json";
import enResources from "../locales/en.json";
import esResources from "../locales/es.json";
i18n
// detect user language
// learn more: https://github.com/i18next/i18next-browser-languageDetector
.use(LanguageDetector)
// pass the i18n instance to react-i18next.
.use(initReactI18next)
// init i18next
// for all options read: https://www.i18next.com/overview/configuration-options
.init({
debug: true,
fallbackLng: "en",
interpolation: {
escapeValue: false,
},
resources: {
ca: catResources,
en: enResources,
es: esResources,
},
});
export default i18n;

View File

@ -0,0 +1,9 @@
{
"translation": {
"title": "Presupuestador para distribuidores",
"LoginPage": {
"title": "Pressupostador per a distribuïdors",
"description": "Introdueixi la seva adreça de correu electrònic i contrasenya per accedir-hi"
}
}
}

View File

@ -0,0 +1,17 @@
{
"translation": {
"title": "Presupuestador para distribuidores",
"login_page": {
"title": "Presupuestador para distribuidores",
"description": "Introduzca su dirección de correo electrónico y contraseña para acceder",
"email_label": "Email",
"email_placeholder": "micorreo@ejemplo.com",
"password_label": "Password",
"forgotten_password": "¿Has olvidado tu contraseña?",
"become_dealer": "¿Quieres ser distribuidor de Uecko?",
"contact_us": "Contact us",
"login": "Login",
"error": "Error"
}
}
}

View File

@ -0,0 +1,39 @@
{
"translation": {
"common": {
"cancel": "Cancelar",
"no": "No",
"yes": "Sí",
"Accept": "Aceptar"
},
"main_menu": {
"home": "Inicio",
"settings": "Ajustes",
"dealers": "Distribuidores",
"catalog": "Catálogo",
"quotes": "Cotizaciones",
"search_placeholder": "Buscar productos, distribuidores, etc...",
"user": {
"user_menu": "Menú del usuario",
"my_account": "Mi cuenta",
"profile": "Perfil",
"settings": "Ajustes",
"support": "Soporte",
"logout": "Salir"
},
"logout": {}
},
"login_page": {
"title": "Presupuestador para distribuidores",
"description": "Introduzca su dirección de correo electrónico y contraseña para acceder",
"email_label": "Email",
"email_placeholder": "micorreo@ejemplo.com",
"password_label": "Contraseña",
"forgotten_password": "¿Has olvidado tu contraseña?",
"become_dealer": "¿Quieres ser distribuidor de Uecko?",
"contact_us": "Contacta con nosotros",
"login": "Entrar",
"error": "Error"
}
}
}

View File

@ -3,6 +3,8 @@ import ReactDOM from "react-dom/client";
import App from "./App.tsx";
import "./index.css";
import "./lib/i18n";
ReactDOM.createRoot(document.getElementById("uecko")!).render(
<React.StrictMode>
<App />

View File

@ -1,232 +0,0 @@
import { CircleUser, LogOutIcon, Menu, Package2Icon, Search, UserIcon } from "lucide-react";
import {
Button,
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Checkbox,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuTrigger,
Input,
Sheet,
SheetContent,
SheetTrigger,
} from "@/ui";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/ui";
import { UeckoLogo } from "@/components/UeckoLogo/UeckoLogo";
import { SyntheticEvent, useState } from "react";
import { Link } from "react-router-dom";
export const DashboardPage = () => {
const [logoutAlertVisible, setLogoutAlertVisible] = useState<boolean>(false);
const openLogoutAlert = (event: Event) => {
event.preventDefault();
setLogoutAlertVisible(true);
return;
};
const closeLogoutAlert = (event: SyntheticEvent) => {
event.preventDefault();
setLogoutAlertVisible(false);
return;
};
return (
<div className='flex flex-col w-full min-h-screen'>
<header className='sticky top-0 flex items-center h-16 gap-4 px-4 border-b bg-background md:px-6'>
<nav className='flex-col hidden gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6'>
<Link to='/' className='flex items-center font-semibold'>
<UeckoLogo className='w-24' />
<span className='sr-only'>Uecko</span>
</Link>
<Link to='#' className='transition-colors text-muted-foreground hover:text-foreground'>
Dashboard
</Link>
<Link to='#' className='transition-colors text-muted-foreground hover:text-foreground'>
Orders
</Link>
<Link to='#' className='transition-colors text-muted-foreground hover:text-foreground'>
Products
</Link>
<Link to='#' className='transition-colors text-muted-foreground hover:text-foreground'>
Customers
</Link>
<Link to='#' className='transition-colors text-foreground hover:text-foreground'>
Settings
</Link>
</nav>
<Sheet>
<SheetTrigger asChild>
<Button variant='outline' size='icon' className='shrink-0 md:hidden'>
<Menu className='w-5 h-5' />
<span className='sr-only'>Toggle navigation menu</span>
</Button>
</SheetTrigger>
<SheetContent side='left'>
<nav className='grid gap-6 text-lg font-medium'>
<Link to='#' className='flex items-center gap-2 text-lg font-semibold'>
<Package2Icon className='w-6 h-6' />
<span className='sr-only'>Acme Inc</span>
</Link>
<Link to='#' className='text-muted-foreground hover:text-foreground'>
Dashboard
</Link>
<Link to='#' className='text-muted-foreground hover:text-foreground'>
Orders
</Link>
<Link to='#' className='text-muted-foreground hover:text-foreground'>
Products
</Link>
<Link to='#' className='text-muted-foreground hover:text-foreground'>
Customers
</Link>
<Link to='#' className='hover:text-foreground'>
Settings
</Link>
</nav>
</SheetContent>
</Sheet>
<div className='flex items-center w-full gap-4 md:ml-auto md:gap-2 lg:gap-4'>
<form className='flex-1 ml-auto sm:flex-initial'>
<div className='relative'>
<Search className='absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground' />
<Input
type='search'
placeholder='Search products...'
className='pl-8 sm:w-[300px] md:w-[200px] lg:w-[300px]'
/>
</div>
</form>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant='secondary' size='icon' className='rounded-full'>
<CircleUser className='w-5 h-5' />
<span className='sr-only'>Toggle user menu</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align='end' className='w-56'>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>
<UserIcon className='w-4 h-4 mr-2' />
<span>Profile</span>
<DropdownMenuShortcut>P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>Settings</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem onSelect={openLogoutAlert}>
<LogOutIcon className='w-4 h-4 mr-2' />
<span>Log out</span>
<DropdownMenuShortcut>Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
<main className='flex min-h-[calc(100vh_-_theme(spacing.16))] flex-1 flex-col gap-4 bg-muted/40 p-4 md:gap-8 md:p-10'>
<div className='grid w-full max-w-6xl gap-2 mx-auto'>
<h1 className='text-3xl font-semibold'>Settings</h1>
</div>
<div className='mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]'>
<nav className='grid gap-4 text-sm text-muted-foreground' x-chunk='dashboard-04-chunk-0'>
<Link to='#' className='font-semibold text-primary'>
General
</Link>
<Link to='#'>Security</Link>
<Link to='#'>Integrations</Link>
<Link to='#'>Support</Link>
<Link to='#'>Organizations</Link>
<Link to='#'>Advanced</Link>
</nav>
<div className='grid gap-6'>
<Card x-chunk='dashboard-04-chunk-1'>
<CardHeader>
<CardTitle>Store Name</CardTitle>
<CardDescription>Used to identify your store in the marketplace.</CardDescription>
</CardHeader>
<CardContent>
<form>
<Input placeholder='Store Name' />
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<Card x-chunk='dashboard-04-chunk-2'>
<CardHeader>
<CardTitle>Plugins Directory</CardTitle>
<CardDescription>
The directory within your project, in which your plugins are located.
</CardDescription>
</CardHeader>
<CardContent>
<form className='flex flex-col gap-4'>
<Input placeholder='Project Name' defaultValue='/content/plugins' />
<div className='flex items-center space-x-2'>
<Checkbox id='include' defaultChecked />
<label
htmlFor='include'
className='text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
>
Allow administrators to change the directory.
</label>
</div>
</form>
</CardContent>
<CardFooter className='px-6 py-4 border-t'>
<Button>Save</Button>
</CardFooter>
</Card>
<AlertDialog open={logoutAlertVisible}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
<AlertDialogDescription>
This action cannot be undone. This will permanently delete your account and
remove your data from our servers.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>
<Link to='#' onClick={closeLogoutAlert}>
Cancel
</Link>
</AlertDialogCancel>
<AlertDialogAction>
<Link to='/logout'>Continue</Link>
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</div>
</main>
</div>
);
};

View File

@ -1 +0,0 @@
export * from "./DashboardPage";

View File

@ -1,4 +0,0 @@
export * from "./DashboardPage";
export * from "./ErrorPage";
export * from "./LoginPage";
export * from "./LogoutPage";

View File

@ -5,6 +5,7 @@ export * from "./aspect-ratio";
export * from "./autosize-textarea";
export * from "./avatar";
export * from "./badge";
export * from "./breadcrumb";
export * from "./button";
export * from "./calendar";
export * from "./card";

View File

@ -7,12 +7,7 @@
"esModuleInterop": true,
"resolveJsonModule": true
},
"references": [{ "path": "./shared/tsconfig.json" }],
"include": [
"server/**/*.ts",
"client/**/*.ts",
"shared/**/*.ts",
"server/src/contexts/users/application/CreateUser.useCase.ts"
],
/*"references": [{ "path": "./shared/tsconfig.json" }],*/
"include": ["server/**/*.ts", "client/**/*.ts", "shared/**/*.ts"],
"exclude": ["**/node_modules"]
}