From a2995234eeb46bb9a70be65f10884a790419a53c Mon Sep 17 00:00:00 2001 From: David Arranz Date: Sun, 9 Jun 2024 22:04:46 +0200 Subject: [PATCH] . --- client/package.json | 3 + client/src/Routes.tsx | 27 +- client/src/{pages => app}/ErrorPage.tsx | 0 client/src/{pages => app}/LoginPage.tsx | 31 +- client/src/{pages => app}/LogoutPage.tsx | 0 client/src/app/StartPage.tsx | 25 ++ client/src/app/auth/AuthLayout.tsx | 11 - client/src/app/auth/index.ts | 1 - client/src/app/catalog/index.ts | 1 + client/src/app/catalog/list.tsx | 312 ++++++++++++++++++ client/src/app/dashboard/index.tsx | 96 ++++++ client/src/app/dealers/index.ts | 1 + client/src/app/index.ts | 8 + client/src/app/quotes/index.ts | 1 + .../components/CustomDialog/CustomDialog.tsx | 55 +++ client/src/components/CustomDialog/index.ts | 1 + client/src/components/Layout/Layout.tsx | 7 + .../src/components/Layout/LayoutContent.tsx | 11 + client/src/components/Layout/LayoutHeader.tsx | 91 +++++ .../Layout/components/UserAvatar.tsx | 20 ++ .../Layout/components/UserButton.tsx | 87 +++++ client/src/components/Layout/index.ts | 3 + client/src/components/UeckoLogo/index.ts | 1 + client/src/components/index.ts | 2 + client/src/index.css | 78 ++--- client/src/lib/axios/axiosInstance.ts | 77 +---- .../src/lib/axios/createAxiosAuthActions.ts | 42 ++- client/src/lib/axios/setupInterceptors.ts | 30 +- client/src/lib/helpers/index.ts | 36 ++ client/src/lib/hooks/index.ts | 1 + client/src/lib/hooks/useAuth/index.ts | 1 + .../src/lib/hooks/useAuth/useGetIdentity.ts | 14 + .../src/lib/hooks/useAuth/useIsLoggedIn.tsx | 16 +- client/src/lib/hooks/useAuth/useLogin.tsx | 2 + client/src/lib/hooks/useCustomDialog/index.ts | 1 + .../hooks/useCustomDialog/useCustomDialog.tsx | 66 ++++ client/src/lib/hooks/useTranslation/index.ts | 0 .../hooks/useTranslation/useTranslation.tsx | 0 .../hooks/useUnsavedChangesNotifier/index.ts | 1 + .../useUnsavedChangesNotifier.tsx | 16 + client/src/lib/i18n.ts | 29 ++ client/src/locales/ca.json | 9 + client/src/locales/en.json | 17 + client/src/locales/es.json | 39 +++ client/src/main.tsx | 2 + .../src/pages/DashboardPage/DashboardPage.tsx | 232 ------------- client/src/pages/DashboardPage/index.ts | 1 - client/src/pages/index.ts | 4 - client/src/ui/index.ts | 1 + tsconfig.json | 9 +- 50 files changed, 1087 insertions(+), 432 deletions(-) rename client/src/{pages => app}/ErrorPage.tsx (100%) rename client/src/{pages => app}/LoginPage.tsx (81%) rename client/src/{pages => app}/LogoutPage.tsx (100%) create mode 100644 client/src/app/StartPage.tsx delete mode 100644 client/src/app/auth/AuthLayout.tsx delete mode 100644 client/src/app/auth/index.ts create mode 100644 client/src/app/catalog/index.ts create mode 100644 client/src/app/catalog/list.tsx create mode 100644 client/src/app/dashboard/index.tsx create mode 100644 client/src/app/dealers/index.ts create mode 100644 client/src/app/index.ts create mode 100644 client/src/app/quotes/index.ts create mode 100644 client/src/components/CustomDialog/CustomDialog.tsx create mode 100644 client/src/components/CustomDialog/index.ts create mode 100644 client/src/components/Layout/Layout.tsx create mode 100644 client/src/components/Layout/LayoutContent.tsx create mode 100644 client/src/components/Layout/LayoutHeader.tsx create mode 100644 client/src/components/Layout/components/UserAvatar.tsx create mode 100644 client/src/components/Layout/components/UserButton.tsx create mode 100644 client/src/components/Layout/index.ts create mode 100644 client/src/lib/hooks/useAuth/useGetIdentity.ts create mode 100644 client/src/lib/hooks/useCustomDialog/index.ts create mode 100644 client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx create mode 100644 client/src/lib/hooks/useTranslation/index.ts create mode 100644 client/src/lib/hooks/useTranslation/useTranslation.tsx create mode 100644 client/src/lib/hooks/useUnsavedChangesNotifier/index.ts create mode 100644 client/src/lib/hooks/useUnsavedChangesNotifier/useUnsavedChangesNotifier.tsx create mode 100644 client/src/lib/i18n.ts create mode 100644 client/src/locales/ca.json create mode 100644 client/src/locales/en.json create mode 100644 client/src/locales/es.json delete mode 100644 client/src/pages/DashboardPage/DashboardPage.tsx delete mode 100644 client/src/pages/DashboardPage/index.ts delete mode 100644 client/src/pages/index.ts diff --git a/client/package.json b/client/package.json index 608a98f..e14eea9 100644 --- a/client/package.json +++ b/client/package.json @@ -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", diff --git a/client/src/Routes.tsx b/client/src/Routes.tsx index 15dd2c0..9200db3 100644 --- a/client/src/Routes.tsx +++ b/client/src/Routes.tsx @@ -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:
Service Page
, - }, - { - path: "/about-us", - element:
About Us
, + 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 diff --git a/client/src/pages/ErrorPage.tsx b/client/src/app/ErrorPage.tsx similarity index 100% rename from client/src/pages/ErrorPage.tsx rename to client/src/app/ErrorPage.tsx diff --git a/client/src/pages/LoginPage.tsx b/client/src/app/LoginPage.tsx similarity index 81% rename from client/src/pages/LoginPage.tsx rename to client/src/app/LoginPage.tsx index e0e5db2..b683644 100644 --- a/client/src/pages/LoginPage.tsx +++ b/client/src/app/LoginPage.tsx @@ -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 = async (data) => { - console.log(data); try { setLoading(true); login({ email: data.email, password: data.password }); @@ -74,8 +75,12 @@ export const LoginPage = () => { - Presupuestador para distribuidores - Enter your email below to login to your account + + + + + +
@@ -84,9 +89,9 @@ export const LoginPage = () => {
{
{ errors={form.formState.errors} />
- ¿Has olvidado tu contraseña? +
- Contacta con nosotros +
@@ -115,20 +120,22 @@ export const LoginPage = () => { {form.formState.errors.root?.message && ( - Heads up! + + + {form.formState.errors.root?.message} )}
- ¿Quieres ser distribuidor de Uecko? +
- Contacta con nosotros +
diff --git a/client/src/pages/LogoutPage.tsx b/client/src/app/LogoutPage.tsx similarity index 100% rename from client/src/pages/LogoutPage.tsx rename to client/src/app/LogoutPage.tsx diff --git a/client/src/app/StartPage.tsx b/client/src/app/StartPage.tsx new file mode 100644 index 0000000..a28f291 --- /dev/null +++ b/client/src/app/StartPage.tsx @@ -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 ; + } + + if (!authenticated) { + return ( + + ); + } + + return ; +}; diff --git a/client/src/app/auth/AuthLayout.tsx b/client/src/app/auth/AuthLayout.tsx deleted file mode 100644 index 9c2280f..0000000 --- a/client/src/app/auth/AuthLayout.tsx +++ /dev/null @@ -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 ( - - - - ); -}; diff --git a/client/src/app/auth/index.ts b/client/src/app/auth/index.ts deleted file mode 100644 index 30e3bd9..0000000 --- a/client/src/app/auth/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./AuthLayout"; diff --git a/client/src/app/catalog/index.ts b/client/src/app/catalog/index.ts new file mode 100644 index 0000000..491ccf0 --- /dev/null +++ b/client/src/app/catalog/index.ts @@ -0,0 +1 @@ +export * from "./list"; diff --git a/client/src/app/catalog/list.tsx b/client/src/app/catalog/list.tsx new file mode 100644 index 0000000..52f8af0 --- /dev/null +++ b/client/src/app/catalog/list.tsx @@ -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 ( + + + + +
+ + All + Active + Draft + + Archived + + +
+ + + + + + Filter by + + Active + Draft + Archived + + + + +
+
+ + + + Products + + Manage your products and view their sales performance. + + + + + + + + Image + + Name + Status + Price + Total Sales + Created at + + Actions + + + + + + + Product image + + Laser Lemonade Machine + + Draft + + $499.99 + 25 + 2023-07-12 10:42 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Hypernova Headphones + + Active + + $129.99 + 100 + 2023-10-18 03:21 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + AeroGlow Desk Lamp + + Active + + $39.99 + 50 + 2023-11-29 08:15 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + TechTonic Energy Drink + + Draft + + $2.99 + 0 + 2023-12-25 11:59 PM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Gamer Gear Pro Controller + + Active + + $59.99 + 75 + 2024-01-01 12:00 AM + + + + + + + Actions + Edit + Delete + + + + + + + Product image + + Luminous VR Headset + + Active + + $199.99 + 30 + 2024-02-14 02:14 PM + + + + + + + Actions + Edit + Delete + + + + + +
+
+ +
+ Showing 1-10 of 32 products +
+
+
+
+
+
+
+ ); +}; diff --git a/client/src/app/dashboard/index.tsx b/client/src/app/dashboard/index.tsx new file mode 100644 index 0000000..3236f15 --- /dev/null +++ b/client/src/app/dashboard/index.tsx @@ -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(false); + + const openLogoutAlert = (event: Event) => { + event.preventDefault(); + setLogoutAlertVisible(true); + return; + }; + + const closeLogoutAlert = (event: SyntheticEvent) => { + event.preventDefault(); + setLogoutAlertVisible(false); + return; + }; + + return ( + + +
+
+

Settings

+
+
+ +
+ + + Store Name + Used to identify your store in the marketplace. + + + + + + + + + + + + + Plugins Directory + + The directory within your project, in which your plugins are located. + + + +
+ +
+ + +
+
+
+ + + +
+
+
+
+
+ ); +}; diff --git a/client/src/app/dealers/index.ts b/client/src/app/dealers/index.ts new file mode 100644 index 0000000..491ccf0 --- /dev/null +++ b/client/src/app/dealers/index.ts @@ -0,0 +1 @@ +export * from "./list"; diff --git a/client/src/app/index.ts b/client/src/app/index.ts new file mode 100644 index 0000000..41053fe --- /dev/null +++ b/client/src/app/index.ts @@ -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"; diff --git a/client/src/app/quotes/index.ts b/client/src/app/quotes/index.ts new file mode 100644 index 0000000..491ccf0 --- /dev/null +++ b/client/src/app/quotes/index.ts @@ -0,0 +1 @@ +export * from "./list"; diff --git a/client/src/components/CustomDialog/CustomDialog.tsx b/client/src/components/CustomDialog/CustomDialog.tsx new file mode 100644 index 0000000..8b96df2 --- /dev/null +++ b/client/src/components/CustomDialog/CustomDialog.tsx @@ -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 ( + + + + {title} + {description} + + + + + {cancelLabel} + + + + + {confirmLabel} + + + + + + ); +}; diff --git a/client/src/components/CustomDialog/index.ts b/client/src/components/CustomDialog/index.ts new file mode 100644 index 0000000..c41a30c --- /dev/null +++ b/client/src/components/CustomDialog/index.ts @@ -0,0 +1 @@ +export * from "./CustomDialog"; diff --git a/client/src/components/Layout/Layout.tsx b/client/src/components/Layout/Layout.tsx new file mode 100644 index 0000000..939034e --- /dev/null +++ b/client/src/components/Layout/Layout.tsx @@ -0,0 +1,7 @@ +import { PropsWithChildren } from "react"; + +export const Layout = ({ children }: PropsWithChildren) => { + return
{children}
; +}; + +Layout.displayName = "Layout"; diff --git a/client/src/components/Layout/LayoutContent.tsx b/client/src/components/Layout/LayoutContent.tsx new file mode 100644 index 0000000..8c70fa7 --- /dev/null +++ b/client/src/components/Layout/LayoutContent.tsx @@ -0,0 +1,11 @@ +import { PropsWithChildren } from "react"; + +export const LayoutContent = ({ children }: PropsWithChildren) => { + return ( +
+ {children} +
+ ); +}; + +LayoutContent.displayName = "LayoutContent"; diff --git a/client/src/components/Layout/LayoutHeader.tsx b/client/src/components/Layout/LayoutHeader.tsx new file mode 100644 index 0000000..e40f07b --- /dev/null +++ b/client/src/components/Layout/LayoutHeader.tsx @@ -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 ( +
+ + + + + + + + + +
+
+
+ + +
+
+ +
+
+ ); +}; + +LayoutHeader.displayName = "LayoutHeader"; diff --git a/client/src/components/Layout/components/UserAvatar.tsx b/client/src/components/Layout/components/UserAvatar.tsx new file mode 100644 index 0000000..8aa2d88 --- /dev/null +++ b/client/src/components/Layout/components/UserAvatar.tsx @@ -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 ( + + + + + + + ); +}; diff --git a/client/src/components/Layout/components/UserButton.tsx b/client/src/components/Layout/components/UserButton.tsx new file mode 100644 index 0000000..7ba95ca --- /dev/null +++ b/client/src/components/Layout/components/UserButton.tsx @@ -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 ( + + + + + + {t("main_menu.user.my_account")} + + + + + {t("main_menu.user.profile")} + ⇧⌘P + + + + {t("main_menu.user.settings")} + + + + {t("main_menu.user.support")} + + + + openLogoutDialog()}> + + {t("main_menu.user.logout")} + ⇧⌘Q + + + {LogoutDialog} + + ); +}; diff --git a/client/src/components/Layout/index.ts b/client/src/components/Layout/index.ts new file mode 100644 index 0000000..3d80f8d --- /dev/null +++ b/client/src/components/Layout/index.ts @@ -0,0 +1,3 @@ +export * from "./Layout"; +export * from "./LayoutContent"; +export * from "./LayoutHeader"; diff --git a/client/src/components/UeckoLogo/index.ts b/client/src/components/UeckoLogo/index.ts index e69de29..a9ed018 100644 --- a/client/src/components/UeckoLogo/index.ts +++ b/client/src/components/UeckoLogo/index.ts @@ -0,0 +1 @@ +export * from "./UeckoLogo"; diff --git a/client/src/components/index.ts b/client/src/components/index.ts index d503e76..8c22577 100644 --- a/client/src/components/index.ts +++ b/client/src/components/index.ts @@ -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"; diff --git a/client/src/index.css b/client/src/index.css index 1ffda70..76e0875 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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%; } } diff --git a/client/src/lib/axios/axiosInstance.ts b/client/src/lib/axios/axiosInstance.ts index 6240f3e..b34e895 100644 --- a/client/src/lib/axios/axiosInstance.ts +++ b/client/src/lib/axios/axiosInstance.ts @@ -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 = (config: AxiosRequestConfig) => - axiosInstance.request(config); - -export default axios;*/ - -export const createAxiosInstance = () => axiosInstance; +export const createAxiosInstance = () => + setupInterceptorsTo(axiosClient.create(defaultAxiosRequestConfig)); diff --git a/client/src/lib/axios/createAxiosAuthActions.ts b/client/src/lib/axios/createAxiosAuthActions.ts index bd5576d..9fbcecd 100644 --- a/client/src/lib/axios/createAxiosAuthActions.ts +++ b/client/src/lib/axios/createAxiosAuthActions.ts @@ -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({ + const result = await httpClient.request({ 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({ + 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, diff --git a/client/src/lib/axios/setupInterceptors.ts b/client/src/lib/axios/setupInterceptors.ts index 2d88e4f..7dad088 100644 --- a/client/src/lib/axios/setupInterceptors.ts +++ b/client/src/lib/axios/setupInterceptors.ts @@ -1,20 +1,14 @@ -import { - AxiosError, - AxiosInstance, - AxiosResponse, - InternalAxiosRequestConfig, -} from "axios"; - -//use(onFulfilled?: ((value: V) => V | Promise) | 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 => { 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; diff --git a/client/src/lib/helpers/index.ts b/client/src/lib/helpers/index.ts index e69de29..361a458 100644 --- a/client/src/lib/helpers/index.ts +++ b/client/src/lib/helpers/index.ts @@ -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. 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]; +}; diff --git a/client/src/lib/hooks/index.ts b/client/src/lib/hooks/index.ts index f3f4ba4..1b35fd9 100644 --- a/client/src/lib/hooks/index.ts +++ b/client/src/lib/hooks/index.ts @@ -12,4 +12,5 @@ export * from "./useUrlId"; */ export * from "./useAuth"; +export * from "./useCustomDialog"; export * from "./useTheme"; diff --git a/client/src/lib/hooks/useAuth/index.ts b/client/src/lib/hooks/useAuth/index.ts index fdd630d..121c4d7 100644 --- a/client/src/lib/hooks/useAuth/index.ts +++ b/client/src/lib/hooks/useAuth/index.ts @@ -1,5 +1,6 @@ export * from "./AuthActions"; export * from "./AuthContext"; export * from "./useAuth"; +export * from "./useGetIdentity"; export * from "./useIsLoggedIn"; export * from "./useLogin"; diff --git a/client/src/lib/hooks/useAuth/useGetIdentity.ts b/client/src/lib/hooks/useAuth/useGetIdentity.ts new file mode 100644 index 0000000..b54123e --- /dev/null +++ b/client/src/lib/hooks/useAuth/useGetIdentity.ts @@ -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, + }); +}; diff --git a/client/src/lib/hooks/useAuth/useIsLoggedIn.tsx b/client/src/lib/hooks/useAuth/useIsLoggedIn.tsx index fd8da87..6b75ee6 100644 --- a/client/src/lib/hooks/useAuth/useIsLoggedIn.tsx +++ b/client/src/lib/hooks/useAuth/useIsLoggedIn.tsx @@ -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 -) => { +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, }); }; diff --git a/client/src/lib/hooks/useAuth/useLogin.tsx b/client/src/lib/hooks/useAuth/useLogin.tsx index 7c924ef..0336585 100644 --- a/client/src/lib/hooks/useAuth/useLogin.tsx +++ b/client/src/lib/hooks/useAuth/useLogin.tsx @@ -25,6 +25,8 @@ export const useLogin = (params?: UseMutationOptions { const { message } = error; + console.error(message); + toast.error(message); if (onError) { diff --git a/client/src/lib/hooks/useCustomDialog/index.ts b/client/src/lib/hooks/useCustomDialog/index.ts new file mode 100644 index 0000000..88a7c69 --- /dev/null +++ b/client/src/lib/hooks/useCustomDialog/index.ts @@ -0,0 +1 @@ +export * from "./useCustomDialog"; diff --git a/client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx b/client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx new file mode 100644 index 0000000..92ec2bc --- /dev/null +++ b/client/src/lib/hooks/useCustomDialog/useCustomDialog.tsx @@ -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) => { + const [isOpen, setIsOpen] = useState(false); + const [dialogConfig, setDialogConfig] = useState({ + 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 ? ( + + ) : ( + <> + ); + + return { + openDialog, + cancelDialog: handleCancel, + confirmDialog: handleConfirm, + DialogComponent, + }; +}; diff --git a/client/src/lib/hooks/useTranslation/index.ts b/client/src/lib/hooks/useTranslation/index.ts new file mode 100644 index 0000000..e69de29 diff --git a/client/src/lib/hooks/useTranslation/useTranslation.tsx b/client/src/lib/hooks/useTranslation/useTranslation.tsx new file mode 100644 index 0000000..e69de29 diff --git a/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts b/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts new file mode 100644 index 0000000..5cdc87b --- /dev/null +++ b/client/src/lib/hooks/useUnsavedChangesNotifier/index.ts @@ -0,0 +1 @@ +export * from "./useUnsavedChangesNotifier"; diff --git a/client/src/lib/hooks/useUnsavedChangesNotifier/useUnsavedChangesNotifier.tsx b/client/src/lib/hooks/useUnsavedChangesNotifier/useUnsavedChangesNotifier.tsx new file mode 100644 index 0000000..dc506ce --- /dev/null +++ b/client/src/lib/hooks/useUnsavedChangesNotifier/useUnsavedChangesNotifier.tsx @@ -0,0 +1,16 @@ +import React from "react"; +import { useCustomDialog } from "../useCustomDialog"; + +type UnsavedChangesNotifierProps = { + translationKey?: string; + message?: string; +}; + +export const UnsavedChangesNotifier: React.FC = () => { + 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}; +}; diff --git a/client/src/lib/i18n.ts b/client/src/lib/i18n.ts new file mode 100644 index 0000000..ec68277 --- /dev/null +++ b/client/src/lib/i18n.ts @@ -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; diff --git a/client/src/locales/ca.json b/client/src/locales/ca.json new file mode 100644 index 0000000..f29db0d --- /dev/null +++ b/client/src/locales/ca.json @@ -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" + } + } +} diff --git a/client/src/locales/en.json b/client/src/locales/en.json new file mode 100644 index 0000000..1f82bbc --- /dev/null +++ b/client/src/locales/en.json @@ -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" + } + } +} diff --git a/client/src/locales/es.json b/client/src/locales/es.json new file mode 100644 index 0000000..5953046 --- /dev/null +++ b/client/src/locales/es.json @@ -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" + } + } +} diff --git a/client/src/main.tsx b/client/src/main.tsx index bdf2012..42bebb3 100644 --- a/client/src/main.tsx +++ b/client/src/main.tsx @@ -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( diff --git a/client/src/pages/DashboardPage/DashboardPage.tsx b/client/src/pages/DashboardPage/DashboardPage.tsx deleted file mode 100644 index a5c32e8..0000000 --- a/client/src/pages/DashboardPage/DashboardPage.tsx +++ /dev/null @@ -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(false); - - const openLogoutAlert = (event: Event) => { - event.preventDefault(); - setLogoutAlertVisible(true); - return; - }; - - const closeLogoutAlert = (event: SyntheticEvent) => { - event.preventDefault(); - setLogoutAlertVisible(false); - return; - }; - - return ( -
-
- - - - - - - - - -
-
-
- - -
-
- - - - - - My Account - - - - - Profile - ⇧⌘P - - Settings - Support - - - - - Log out - ⇧⌘Q - - - -
-
-
-
-

Settings

-
-
- -
- - - Store Name - Used to identify your store in the marketplace. - - -
- -
-
- - - -
- - - Plugins Directory - - The directory within your project, in which your plugins are located. - - - -
- -
- - -
-
-
- - - -
- - - - Are you absolutely sure? - - This action cannot be undone. This will permanently delete your account and - remove your data from our servers. - - - - - - Cancel - - - - Continue - - - - -
-
-
-
- ); -}; diff --git a/client/src/pages/DashboardPage/index.ts b/client/src/pages/DashboardPage/index.ts deleted file mode 100644 index 353b584..0000000 --- a/client/src/pages/DashboardPage/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./DashboardPage"; diff --git a/client/src/pages/index.ts b/client/src/pages/index.ts deleted file mode 100644 index f07a7cd..0000000 --- a/client/src/pages/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from "./DashboardPage"; -export * from "./ErrorPage"; -export * from "./LoginPage"; -export * from "./LogoutPage"; diff --git a/client/src/ui/index.ts b/client/src/ui/index.ts index c315f38..3506a05 100644 --- a/client/src/ui/index.ts +++ b/client/src/ui/index.ts @@ -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"; diff --git a/tsconfig.json b/tsconfig.json index ea08ec5..b96cff4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -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"] }