.
This commit is contained in:
parent
cdb6e7de99
commit
a2995234ee
@ -44,6 +44,8 @@
|
|||||||
"class-variance-authority": "^0.7.0",
|
"class-variance-authority": "^0.7.0",
|
||||||
"cmdk": "^1.0.0",
|
"cmdk": "^1.0.0",
|
||||||
"date-fns": "^3.6.0",
|
"date-fns": "^3.6.0",
|
||||||
|
"i18next": "^23.11.5",
|
||||||
|
"i18next-browser-languagedetector": "^8.0.0",
|
||||||
"joi": "^17.13.1",
|
"joi": "^17.13.1",
|
||||||
"lucide-react": "^0.379.0",
|
"lucide-react": "^0.379.0",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
@ -51,6 +53,7 @@
|
|||||||
"react-day-picker": "^8.10.1",
|
"react-day-picker": "^8.10.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.51.5",
|
"react-hook-form": "^7.51.5",
|
||||||
|
"react-i18next": "^14.1.2",
|
||||||
"react-resizable-panels": "^2.0.19",
|
"react-resizable-panels": "^2.0.19",
|
||||||
"react-router-dom": "^6.23.1",
|
"react-router-dom": "^6.23.1",
|
||||||
"react-secure-storage": "^1.3.2",
|
"react-secure-storage": "^1.3.2",
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
import { RouterProvider, createBrowserRouter } from "react-router-dom";
|
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 { ProtectedRoute } from "./components";
|
||||||
import { DashboardPage, LogoutPage } from "./pages";
|
|
||||||
import { LoginPage } from "./pages/LoginPage";
|
|
||||||
|
|
||||||
export const Routes = () => {
|
export const Routes = () => {
|
||||||
// Define public routes accessible to all users
|
// Define public routes accessible to all users
|
||||||
const routesForPublic = [
|
const routesForPublic = [
|
||||||
{
|
{
|
||||||
path: "/service",
|
path: "/",
|
||||||
element: <div>Service Page</div>,
|
Component: StartPage,
|
||||||
},
|
|
||||||
{
|
|
||||||
path: "/about-us",
|
|
||||||
element: <div>About Us</div>,
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
// Define routes accessible only to authenticated users
|
// Define routes accessible only to authenticated users
|
||||||
const routesForAuthenticatedOnly = [
|
const routesForAuthenticatedOnly = [
|
||||||
|
{
|
||||||
|
path: "/home",
|
||||||
|
Component: DashboardPage,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/catalog",
|
||||||
|
Component: CatalogList,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/profile",
|
path: "/profile",
|
||||||
element: (
|
element: (
|
||||||
@ -48,10 +53,6 @@ export const Routes = () => {
|
|||||||
|
|
||||||
// Define routes accessible only to non-authenticated users
|
// Define routes accessible only to non-authenticated users
|
||||||
const routesForNotAuthenticatedOnly = [
|
const routesForNotAuthenticatedOnly = [
|
||||||
{
|
|
||||||
path: "/",
|
|
||||||
Component: DashboardPage,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: "/login",
|
path: "/login",
|
||||||
Component: LoginPage,
|
Component: LoginPage,
|
||||||
@ -61,8 +62,8 @@ export const Routes = () => {
|
|||||||
// Combine and conditionally include routes based on authentication status
|
// Combine and conditionally include routes based on authentication status
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
...routesForPublic,
|
...routesForPublic,
|
||||||
...routesForNotAuthenticatedOnly,
|
|
||||||
...routesForAuthenticatedOnly,
|
...routesForAuthenticatedOnly,
|
||||||
|
...routesForNotAuthenticatedOnly,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Provide the router configuration using RouterProvider
|
// Provide the router configuration using RouterProvider
|
||||||
|
|||||||
@ -19,12 +19,14 @@ import Joi from "joi";
|
|||||||
import { AlertCircleIcon } from "lucide-react";
|
import { AlertCircleIcon } from "lucide-react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { Trans, useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import SpanishJoiMessages from "../spanish-joi-messages.json";
|
import SpanishJoiMessages from "../spanish-joi-messages.json";
|
||||||
|
|
||||||
type LoginDataForm = ILogin_DTO;
|
type LoginDataForm = ILogin_DTO;
|
||||||
|
|
||||||
export const LoginPage = () => {
|
export const LoginPage = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const { mutate: login } = useLogin({
|
const { mutate: login } = useLogin({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@ -55,7 +57,6 @@ export const LoginPage = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginDataForm> = async (data) => {
|
const onSubmit: SubmitHandler<LoginDataForm> = async (data) => {
|
||||||
console.log(data);
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
login({ email: data.email, password: data.password });
|
login({ email: data.email, password: data.password });
|
||||||
@ -74,8 +75,12 @@ export const LoginPage = () => {
|
|||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<UeckoLogo className='inline-block m-auto mb-6 align-middle max-w-32' />
|
<UeckoLogo className='inline-block m-auto mb-6 align-middle max-w-32' />
|
||||||
<CardTitle>Presupuestador para distribuidores</CardTitle>
|
<CardTitle>
|
||||||
<CardDescription>Enter your email below to login to your account</CardDescription>
|
<Trans i18nKey='login_page.title' />
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
<Trans i18nKey='login_page.description' />
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@ -84,9 +89,9 @@ export const LoginPage = () => {
|
|||||||
<div className='grid gap-6'>
|
<div className='grid gap-6'>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
label='Email'
|
label={t("login_page.email_label")}
|
||||||
type='email'
|
type='email'
|
||||||
placeholder='micorreo@ejemplo.com'
|
placeholder={t("login_page.email_placeholder")}
|
||||||
{...form.register("email", {
|
{...form.register("email", {
|
||||||
required: true,
|
required: true,
|
||||||
})}
|
})}
|
||||||
@ -96,7 +101,7 @@ export const LoginPage = () => {
|
|||||||
<div className='grid gap-6'>
|
<div className='grid gap-6'>
|
||||||
<FormTextField
|
<FormTextField
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
label='Contraseña'
|
label={t("login_page.password_label")}
|
||||||
type='password'
|
type='password'
|
||||||
{...form.register("password", {
|
{...form.register("password", {
|
||||||
required: true,
|
required: true,
|
||||||
@ -104,10 +109,10 @@ export const LoginPage = () => {
|
|||||||
errors={form.formState.errors}
|
errors={form.formState.errors}
|
||||||
/>
|
/>
|
||||||
<div className='mb-4 -mt-2 text-sm'>
|
<div className='mb-4 -mt-2 text-sm'>
|
||||||
¿Has olvidado tu contraseña?
|
<Trans i18nKey='login_page.forgotten_password' />
|
||||||
<br />
|
<br />
|
||||||
<Link to='https://uecko.com/distribuidores' className='underline'>
|
<Link to='https://uecko.com/distribuidores' className='underline'>
|
||||||
Contacta con nosotros
|
<Trans i18nKey='login_page.contact_us' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -115,20 +120,22 @@ export const LoginPage = () => {
|
|||||||
{form.formState.errors.root?.message && (
|
{form.formState.errors.root?.message && (
|
||||||
<Alert variant='destructive'>
|
<Alert variant='destructive'>
|
||||||
<AlertCircleIcon className='w-4 h-4' />
|
<AlertCircleIcon className='w-4 h-4' />
|
||||||
<AlertTitle>Heads up!</AlertTitle>
|
<AlertTitle>
|
||||||
|
<Trans i18nKey='login_page.error' />
|
||||||
|
</AlertTitle>
|
||||||
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
|
<AlertDescription>{form.formState.errors.root?.message}</AlertDescription>
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button disabled={loading} type='submit' className='w-full'>
|
<Button disabled={loading} type='submit' className='w-full'>
|
||||||
Entrar
|
<Trans i18nKey='login_page.login' />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<div className='mt-4 text-sm text-center'>
|
<div className='mt-4 text-sm text-center'>
|
||||||
¿Quieres ser distribuidor de Uecko?
|
<Trans i18nKey='login_page.become_dealer' />
|
||||||
<br />
|
<br />
|
||||||
<Link to='https://uecko.com/distribuidores' className='underline'>
|
<Link to='https://uecko.com/distribuidores' className='underline'>
|
||||||
Contacta con nosotros
|
<Trans i18nKey='login_page.contact_us' />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</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 "./Container";
|
||||||
|
export * from "./CustomDialog";
|
||||||
export * from "./Forms";
|
export * from "./Forms";
|
||||||
|
export * from "./Layout";
|
||||||
export * from "./LoadingIndicator";
|
export * from "./LoadingIndicator";
|
||||||
export * from "./LoadingOverlay";
|
export * from "./LoadingOverlay";
|
||||||
export * from "./ProtectedRoute";
|
export * from "./ProtectedRoute";
|
||||||
|
|||||||
@ -2,51 +2,51 @@
|
|||||||
@tailwind components;
|
@tailwind components;
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/* https://ui.jln.dev/ Light Blue */
|
/* https://ui.jln.dev/ Shivering Harpy Brown */
|
||||||
@layer base {
|
@layer base {
|
||||||
:root {
|
:root {
|
||||||
--background: 210 40% 96.08%;
|
--background: 30 65% 100%;
|
||||||
--foreground: 334 55% 1%;
|
--foreground: 30 75% 0%;
|
||||||
--muted: 214.29 31.82% 91.37%;
|
--muted: 30 17% 95%;
|
||||||
--muted-foreground: 334 9% 37%;
|
--muted-foreground: 30 15% 26%;
|
||||||
--popover: 334 62% 100%;
|
--popover: 0 0% 99%;
|
||||||
--popover-foreground: 334 55% 1%;
|
--popover-foreground: 0 0% 0%;
|
||||||
--card: 334 62% 100%;
|
--card: 0 0% 99%;
|
||||||
--card-foreground: 334 55% 1%;
|
--card-foreground: 0 0% 0%;
|
||||||
--border: 334 5% 95%;
|
--border: 30 4% 91%;
|
||||||
--input: 334 5% 95%;
|
--input: 30 4% 91%;
|
||||||
--primary: 209.23 58.21% 39.41%;
|
--primary: 30 26% 23%;
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 30 26% 83%;
|
||||||
--secondary: 213.75 20.25% 69.02%;
|
--secondary: 30 12% 82%;
|
||||||
--secondary-foreground: 334 0% 100%;
|
--secondary-foreground: 30 12% 22%;
|
||||||
--accent: 214.29 31.82% 91.37%;
|
--accent: 30 22% 76%;
|
||||||
--accent-foreground: 334 20% 22%;
|
--accent-foreground: 30 22% 16%;
|
||||||
--destructive: 348.37 78.4% 49.02%;
|
--destructive: 18 91% 33%;
|
||||||
--destructive-foreground: 18 0% 100%;
|
--destructive-foreground: 0 0% 100%;
|
||||||
--ring: 209.23 58.21% 39.41%;
|
--ring: 30 26% 23%;
|
||||||
--radius: 0.5rem;
|
--radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dark {
|
.dark {
|
||||||
--background: 222.22 47.37% 11.18%;
|
--background: 30 53% 1%;
|
||||||
--foreground: 334 34% 98%;
|
--foreground: 30 11% 98%;
|
||||||
--muted: 215.38 16.32% 46.86%;
|
--muted: 30 17% 5%;
|
||||||
--muted-foreground: 334 0% 87.69%;
|
--muted-foreground: 30 15% 74%;
|
||||||
--popover: 217.24 32.58% 17.45%;
|
--popover: 30 53% 2%;
|
||||||
--popover-foreground: 334 34% 98%;
|
--popover-foreground: 30 11% 99%;
|
||||||
--card: 217.24 32.58% 17.45%;
|
--card: 30 53% 2%;
|
||||||
--card-foreground: 334 34% 98%;
|
--card-foreground: 30 11% 99%;
|
||||||
--border: 334 0% 32.31%;
|
--border: 30 4% 14%;
|
||||||
--input: 215.29 25% 26.67%;
|
--input: 30 4% 14%;
|
||||||
--primary: 227.56 53.78% 49.22%;
|
--primary: 30 26% 23%;
|
||||||
--primary-foreground: 0 0% 100%;
|
--primary-foreground: 30 26% 83%;
|
||||||
--secondary: 214.29 5.04% 27.25%;
|
--secondary: 30 7% 15%;
|
||||||
--secondary-foreground: 334 0% 100%;
|
--secondary-foreground: 30 7% 75%;
|
||||||
--accent: 222.22 47.37% 11.18%;
|
--accent: 30 16% 24%;
|
||||||
--accent-foreground: 226.73 0% 100%;
|
--accent-foreground: 30 16% 84%;
|
||||||
--destructive: 358.82 84.44% 64.71%;
|
--destructive: 18 91% 59%;
|
||||||
--destructive-foreground: 0 0% 100%;
|
--destructive-foreground: 0 0% 0%;
|
||||||
--ring: 227.56 53.78% 49.22%;
|
--ring: 30 26% 23%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -42,79 +42,6 @@ export const defaultAxiosRequestConfig = {
|
|||||||
/**
|
/**
|
||||||
* Creates an initial 'axios' instance with custom settings.
|
* Creates an initial 'axios' instance with custom settings.
|
||||||
*/
|
*/
|
||||||
const axiosInstance = setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig));
|
|
||||||
|
|
||||||
/**
|
export const createAxiosInstance = () =>
|
||||||
* Handle all responses.
|
setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig));
|
||||||
*/
|
|
||||||
/*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;
|
|
||||||
|
|||||||
@ -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 secureLocalStorage from "react-secure-storage";
|
||||||
import { IAuthActions } from "../hooks";
|
import { IAuthActions } from "../hooks";
|
||||||
import { createAxiosInstance } from "./axiosInstance";
|
import { createAxiosInstance } from "./axiosInstance";
|
||||||
@ -10,7 +10,7 @@ export const createAxiosAuthActions = (
|
|||||||
login: async ({ email, password }: ILogin_DTO) => {
|
login: async ({ email, password }: ILogin_DTO) => {
|
||||||
// eslint-disable-next-line no-useless-catch
|
// eslint-disable-next-line no-useless-catch
|
||||||
try {
|
try {
|
||||||
const { data } = await httpClient.request<ILogin_Response_DTO>({
|
const result = await httpClient.request<ILogin_Response_DTO>({
|
||||||
url: `${apiUrl}/auth/login`,
|
url: `${apiUrl}/auth/login`,
|
||||||
method: "POST",
|
method: "POST",
|
||||||
data: {
|
data: {
|
||||||
@ -19,7 +19,8 @@ export const createAxiosAuthActions = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
secureLocalStorage.setItem("uecko", JSON.stringify(data));
|
const { data } = result;
|
||||||
|
secureLocalStorage.setItem("uecko.auth", data);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@ -36,28 +37,51 @@ export const createAxiosAuthActions = (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
logout: () => {
|
logout: () => {
|
||||||
secureLocalStorage.removeItem("uecko");
|
secureLocalStorage.clear();
|
||||||
|
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
success: true,
|
success: true,
|
||||||
redirectTo: "/login",
|
redirectTo: "/login",
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
check: () => {
|
check: () => {
|
||||||
const data: ILogin_Response_DTO = JSON.parse(
|
const profile = secureLocalStorage.getItem("uecko.auth") as ILogin_Response_DTO;
|
||||||
secureLocalStorage.getItem("uecko")?.toString() || ""
|
|
||||||
);
|
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
data.token
|
profile?.token
|
||||||
? {
|
? {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
}
|
}
|
||||||
: { authenticated: false, redirectTo: "/login" }
|
: { 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) => {
|
onError: (error: any) => {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
secureLocalStorage.removeItem("uecko");
|
secureLocalStorage.clear();
|
||||||
return Promise.resolve({
|
return Promise.resolve({
|
||||||
error,
|
error,
|
||||||
logout: true,
|
logout: true,
|
||||||
|
|||||||
@ -1,20 +1,14 @@
|
|||||||
import {
|
import { ILogin_Response_DTO } from "@shared/contexts";
|
||||||
AxiosError,
|
import { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestConfig } from "axios";
|
||||||
AxiosInstance,
|
import secureLocalStorage from "react-secure-storage";
|
||||||
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();*/
|
|
||||||
|
|
||||||
|
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;
|
return request;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -95,9 +89,7 @@ const onResponseError = (error: AxiosError): Promise<AxiosError> => {
|
|||||||
throw error;
|
throw error;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function setupInterceptorsTo(
|
export function setupInterceptorsTo(axiosInstance: AxiosInstance): AxiosInstance {
|
||||||
axiosInstance: AxiosInstance
|
|
||||||
): AxiosInstance {
|
|
||||||
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
axiosInstance.interceptors.request.use(onRequest, onRequestError);
|
||||||
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
axiosInstance.interceptors.response.use(onResponse, onResponseError);
|
||||||
return axiosInstance;
|
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 "./useAuth";
|
||||||
|
export * from "./useCustomDialog";
|
||||||
export * from "./useTheme";
|
export * from "./useTheme";
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
export * from "./AuthActions";
|
export * from "./AuthActions";
|
||||||
export * from "./AuthContext";
|
export * from "./AuthContext";
|
||||||
export * from "./useAuth";
|
export * from "./useAuth";
|
||||||
|
export * from "./useGetIdentity";
|
||||||
export * from "./useIsLoggedIn";
|
export * from "./useIsLoggedIn";
|
||||||
export * from "./useLogin";
|
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 { useAuth } from "@/lib/hooks";
|
||||||
import { UseMutationOptions, useMutation } from "@tanstack/react-query";
|
import { UseQueryOptions, useQuery } from "@tanstack/react-query";
|
||||||
import { useQueryKey } from "../useQueryKey";
|
import { useQueryKey } from "../useQueryKey";
|
||||||
|
|
||||||
export const useIsLoggedIn = (
|
export const useIsLoggedIn = (queryOptions?: UseQueryOptions) => {
|
||||||
params?: UseMutationOptions<AuthActionCheckResponse, Error, unknown>
|
|
||||||
) => {
|
|
||||||
const keys = useQueryKey();
|
const keys = useQueryKey();
|
||||||
const { check } = useAuth();
|
const { check } = useAuth();
|
||||||
|
|
||||||
return useMutation({
|
return useQuery({
|
||||||
mutationKey: keys().auth().action("check").get(),
|
queryKey: keys().auth().action("check").get(),
|
||||||
mutationFn: check,
|
queryFn: check,
|
||||||
...params,
|
...queryOptions,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,6 +25,8 @@ export const useLogin = (params?: UseMutationOptions<AuthActionResponse, Error,
|
|||||||
},
|
},
|
||||||
onError: (error, variables, context) => {
|
onError: (error, variables, context) => {
|
||||||
const { message } = error;
|
const { message } = error;
|
||||||
|
console.error(message);
|
||||||
|
|
||||||
toast.error(message);
|
toast.error(message);
|
||||||
|
|
||||||
if (onError) {
|
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 App from "./App.tsx";
|
||||||
import "./index.css";
|
import "./index.css";
|
||||||
|
|
||||||
|
import "./lib/i18n";
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById("uecko")!).render(
|
ReactDOM.createRoot(document.getElementById("uecko")!).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<App />
|
<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 "./autosize-textarea";
|
||||||
export * from "./avatar";
|
export * from "./avatar";
|
||||||
export * from "./badge";
|
export * from "./badge";
|
||||||
|
export * from "./breadcrumb";
|
||||||
export * from "./button";
|
export * from "./button";
|
||||||
export * from "./calendar";
|
export * from "./calendar";
|
||||||
export * from "./card";
|
export * from "./card";
|
||||||
|
|||||||
@ -7,12 +7,7 @@
|
|||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true
|
||||||
},
|
},
|
||||||
"references": [{ "path": "./shared/tsconfig.json" }],
|
/*"references": [{ "path": "./shared/tsconfig.json" }],*/
|
||||||
"include": [
|
"include": ["server/**/*.ts", "client/**/*.ts", "shared/**/*.ts"],
|
||||||
"server/**/*.ts",
|
|
||||||
"client/**/*.ts",
|
|
||||||
"shared/**/*.ts",
|
|
||||||
"server/src/contexts/users/application/CreateUser.useCase.ts"
|
|
||||||
],
|
|
||||||
"exclude": ["**/node_modules"]
|
"exclude": ["**/node_modules"]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user