.
This commit is contained in:
parent
cdb6e7de99
commit
a2995234ee
@ -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",
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
25
client/src/app/StartPage.tsx
Normal file
25
client/src/app/StartPage.tsx
Normal 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 />;
|
||||
};
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from "./AuthLayout";
|
||||
1
client/src/app/catalog/index.ts
Normal file
1
client/src/app/catalog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./list";
|
||||
312
client/src/app/catalog/list.tsx
Normal file
312
client/src/app/catalog/list.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
96
client/src/app/dashboard/index.tsx
Normal file
96
client/src/app/dashboard/index.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client/src/app/dealers/index.ts
Normal file
1
client/src/app/dealers/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./list";
|
||||
8
client/src/app/index.ts
Normal file
8
client/src/app/index.ts
Normal 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";
|
||||
1
client/src/app/quotes/index.ts
Normal file
1
client/src/app/quotes/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./list";
|
||||
55
client/src/components/CustomDialog/CustomDialog.tsx
Normal file
55
client/src/components/CustomDialog/CustomDialog.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
client/src/components/CustomDialog/index.ts
Normal file
1
client/src/components/CustomDialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./CustomDialog";
|
||||
7
client/src/components/Layout/Layout.tsx
Normal file
7
client/src/components/Layout/Layout.tsx
Normal 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";
|
||||
11
client/src/components/Layout/LayoutContent.tsx
Normal file
11
client/src/components/Layout/LayoutContent.tsx
Normal 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";
|
||||
91
client/src/components/Layout/LayoutHeader.tsx
Normal file
91
client/src/components/Layout/LayoutHeader.tsx
Normal 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";
|
||||
20
client/src/components/Layout/components/UserAvatar.tsx
Normal file
20
client/src/components/Layout/components/UserAvatar.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
87
client/src/components/Layout/components/UserButton.tsx
Normal file
87
client/src/components/Layout/components/UserButton.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
3
client/src/components/Layout/index.ts
Normal file
3
client/src/components/Layout/index.ts
Normal file
@ -0,0 +1,3 @@
|
||||
export * from "./Layout";
|
||||
export * from "./LayoutContent";
|
||||
export * from "./LayoutHeader";
|
||||
@ -0,0 +1 @@
|
||||
export * from "./UeckoLogo";
|
||||
@ -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";
|
||||
|
||||
@ -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%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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];
|
||||
};
|
||||
@ -12,4 +12,5 @@ export * from "./useUrlId";
|
||||
*/
|
||||
|
||||
export * from "./useAuth";
|
||||
export * from "./useCustomDialog";
|
||||
export * from "./useTheme";
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
export * from "./AuthActions";
|
||||
export * from "./AuthContext";
|
||||
export * from "./useAuth";
|
||||
export * from "./useGetIdentity";
|
||||
export * from "./useIsLoggedIn";
|
||||
export * from "./useLogin";
|
||||
|
||||
14
client/src/lib/hooks/useAuth/useGetIdentity.ts
Normal file
14
client/src/lib/hooks/useAuth/useGetIdentity.ts
Normal 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,
|
||||
});
|
||||
};
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
1
client/src/lib/hooks/useCustomDialog/index.ts
Normal file
1
client/src/lib/hooks/useCustomDialog/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./useCustomDialog";
|
||||
66
client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx
Normal file
66
client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx
Normal 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,
|
||||
};
|
||||
};
|
||||
0
client/src/lib/hooks/useTranslation/index.ts
Normal file
0
client/src/lib/hooks/useTranslation/index.ts
Normal file
1
client/src/lib/hooks/useUnsavedChangesNotifier/index.ts
Normal file
1
client/src/lib/hooks/useUnsavedChangesNotifier/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from "./useUnsavedChangesNotifier";
|
||||
@ -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
29
client/src/lib/i18n.ts
Normal 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;
|
||||
9
client/src/locales/ca.json
Normal file
9
client/src/locales/ca.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
17
client/src/locales/en.json
Normal file
17
client/src/locales/en.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
39
client/src/locales/es.json
Normal file
39
client/src/locales/es.json
Normal 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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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 />
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
@ -1 +0,0 @@
|
||||
export * from "./DashboardPage";
|
||||
@ -1,4 +0,0 @@
|
||||
export * from "./DashboardPage";
|
||||
export * from "./ErrorPage";
|
||||
export * from "./LoginPage";
|
||||
export * from "./LogoutPage";
|
||||
@ -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";
|
||||
|
||||
@ -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"]
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user