From 74e631658382ffaf6d2d9f73b0542c388d1446d9 Mon Sep 17 00:00:00 2001 From: David Arranz Date: Mon, 26 Aug 2024 15:58:31 +0200 Subject: [PATCH] . --- client/src/App.tsx | 5 +- .../quotes/components/DownloadQuoteDialog.tsx | 2 +- .../editors/QuoteDetailsCardEditor.tsx | 6 + client/src/app/quotes/list.tsx | 15 +- .../ProtectedRoute/ProtectedRoute.tsx | 23 ++-- client/src/index.css | 20 +++ client/src/locales/en.json | 8 +- client/src/locales/es.json | 8 +- client/src/ui/toast.tsx | 75 +++++----- client/src/ui/toaster.tsx | 10 +- client/src/ui/use-toast.ts | 130 +++++++++--------- 11 files changed, 163 insertions(+), 139 deletions(-) diff --git a/client/src/App.tsx b/client/src/App.tsx index 8aaf056..096ad99 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,9 +1,8 @@ import { AuthProvider, ThemeProvider, UnsavedWarnProvider } from "@/lib/hooks"; -import { TooltipProvider } from "@/ui"; +import { Toaster, TooltipProvider } from "@/ui"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { Suspense } from "react"; -import { ToastContainer } from "react-toastify"; import "react-toastify/dist/ReactToastify.css"; import { Routes } from "./Routes"; import { LoadingOverlay, TailwindIndicator } from "./components"; @@ -30,7 +29,7 @@ function App() { }> - + diff --git a/client/src/app/quotes/components/DownloadQuoteDialog.tsx b/client/src/app/quotes/components/DownloadQuoteDialog.tsx index 7c2b5cf..25c7d7e 100644 --- a/client/src/app/quotes/components/DownloadQuoteDialog.tsx +++ b/client/src/app/quotes/components/DownloadQuoteDialog.tsx @@ -23,7 +23,7 @@ export const DownloadQuoteDialog = (props: DownloadQuoteDialogProps) => { const panelId = useId(); useEffect(() => { - if (!isInProgress && !error && percentage === 100) { + if (isInProgress && !error && percentage === 100) { if (onFinishDownload) { onFinishDownload(); } diff --git a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx index 9e50442..011f87e 100644 --- a/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx +++ b/client/src/app/quotes/components/editors/QuoteDetailsCardEditor.tsx @@ -8,6 +8,7 @@ import { import { DataTableProvider } from "@/lib/hooks"; import { cn } from "@/lib/utils"; import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from "@/ui"; +import { useToast } from "@/ui/use-toast"; import { CurrencyData, Language, Quantity } from "@shared/contexts"; import { ColumnDef } from "@tanstack/react-table"; import { t } from "i18next"; @@ -28,6 +29,7 @@ export const QuoteDetailsCardEditor = ({ language: Language; defaultValues: Readonly<{ [x: string]: any }> | undefined; }) => { + const { toast } = useToast(); const { control, register } = useFormContext(); const [pickerMode] = useState<"dialog" | "panel">("dialog"); @@ -200,6 +202,10 @@ export const QuoteDetailsCardEditor = ({ }, unit_price: article.retail_price, }); + toast({ + title: "Artículo del catálog añadido:", + description: article.description, + }); }, [fieldActions] ); diff --git a/client/src/app/quotes/list.tsx b/client/src/app/quotes/list.tsx index d39216d..e4e531f 100644 --- a/client/src/app/quotes/list.tsx +++ b/client/src/app/quotes/list.tsx @@ -5,7 +5,7 @@ import { QuotesDataTable } from "./components"; import { Button, Tabs, TabsContent, TabsList, TabsTrigger, Toggle } from "@/ui"; import { useToggle } from "@wojtekmaj/react-hooks"; import { t } from "i18next"; -import { InfoIcon, PlusIcon } from "lucide-react"; +import { EyeIcon, EyeOffIcon, PlusIcon } from "lucide-react"; import { useNavigate } from "react-router-dom"; export const QuotesList = () => { @@ -56,8 +56,17 @@ export const QuotesList = () => { pressed={enabledPreview} onPressedChange={toggleEnabledPreview} > - - Quote preview + {enabledPreview ? ( + <> + + {t("common.disable_preview")} + + ) : ( + <> + + {t("common.enable_preview")} + + )} diff --git a/client/src/components/ProtectedRoute/ProtectedRoute.tsx b/client/src/components/ProtectedRoute/ProtectedRoute.tsx index 610c39b..d071373 100644 --- a/client/src/components/ProtectedRoute/ProtectedRoute.tsx +++ b/client/src/components/ProtectedRoute/ProtectedRoute.tsx @@ -1,5 +1,5 @@ import { useGetProfile, useIsLoggedIn } from "@/lib/hooks"; -import React, { useEffect } from "react"; +import React, { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Navigate } from "react-router-dom"; import { LoadingOverlay } from "../LoadingOverlay"; @@ -11,11 +11,17 @@ type ProctectRouteProps = { export const ProtectedRoute = ({ children }: ProctectRouteProps) => { const { isPending, isSuccess, data: { authenticated, redirectTo } = {} } = useIsLoggedIn(); const { data: profile, ...profileStatus } = useGetProfile(); + const { i18n } = useTranslation(); + const [langCode, setLangCode] = useState(i18n.language); + + if (i18n.language !== langCode) { + i18n.changeLanguage(langCode); + } useEffect(() => { - if (profileStatus.isSuccess && i18n.language !== profile?.lang_code) { - i18n.changeLanguage(profile?.lang_code); + if (profileStatus.isSuccess && profile && langCode !== profile.lang_code) { + setLangCode(profile.lang_code); } }, [profile, profileStatus, i18n]); @@ -29,17 +35,6 @@ export const ProtectedRoute = ({ children }: ProctectRouteProps) => { // along to that page after they login, which is a nicer user experience // than dropping them off on the home page. return ; - - console.log("No authenticated"); - return ( - - ); } return <>{children ?? null}; diff --git a/client/src/index.css b/client/src/index.css index 0bf4e26..470d1dc 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -109,4 +109,24 @@ #uecko { @apply flex min-h-screen min-w-[320px] flex-col; } + + .ToastRoot[data-swipe="move"] { + transform: translateX(var(--radix-toast-swipe-move-x)); + } + .ToastRoot[data-swipe="cancel"] { + transform: translateX(0); + transition: transform 200ms ease-out; + } + .ToastRoot[data-swipe="end"] { + animation: slideRight 100ms ease-out; + } + + @keyframes slideRight { + from { + transform: translateX(var(--radix-toast-swipe-end-x)); + } + to { + transform: translateX(100%); + } + } } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 902db6b..8c208b6 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -53,7 +53,9 @@ "remove": "Remove", "archive": "Archive", "duplicate": "Duplicate", - "print": "Print" + "print": "Print", + "disable_preview": "Disable preview", + "enable_preview": "Enable preview" }, "components": { "loading_indicator": { @@ -116,8 +118,10 @@ "subtitle": "", "tabs": { "all": "All", - "emitted": "Emitted", "draft": "Draft", + "emitted": "Emitted", + "accepted": "Accepted", + "rejected": "Rejected", "archived": "Archived" }, "columns": { diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 70d9f1d..bd891de 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -53,7 +53,9 @@ "remove": "Eliminar", "archive": "Archivar", "duplicate": "Duplicar", - "print": "Imprimir" + "print": "Imprimir", + "disable_preview": "Ocultar vista previa", + "enable_preview": "Mostrar vista previa" }, "components": { "LoadingIndicator": { @@ -116,8 +118,10 @@ "subtitle": "", "tabs": { "all": "Todas", - "emitted": "Emitidas", "draft": "Borradores", + "emitted": "Emitidas", + "accepted": "Accepted", + "rejected": "Rejected", "archived": "Archivadas" }, "columns": { diff --git a/client/src/ui/toast.tsx b/client/src/ui/toast.tsx index a822477..17d543f 100644 --- a/client/src/ui/toast.tsx +++ b/client/src/ui/toast.tsx @@ -1,11 +1,11 @@ -import * as React from "react" -import * as ToastPrimitives from "@radix-ui/react-toast" -import { cva, type VariantProps } from "class-variance-authority" -import { X } from "lucide-react" +import * as ToastPrimitives from "@radix-ui/react-toast"; +import { cva, type VariantProps } from "class-variance-authority"; +import { X } from "lucide-react"; +import * as React from "react"; -import { cn } from "@/lib/utils" +import { cn } from "@/lib/utils"; -const ToastProvider = ToastPrimitives.Provider +const ToastProvider = ToastPrimitives.Provider; const ToastViewport = React.forwardRef< React.ElementRef, @@ -14,13 +14,13 @@ const ToastViewport = React.forwardRef< -)) -ToastViewport.displayName = ToastPrimitives.Viewport.displayName +)); +ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", @@ -36,12 +36,11 @@ const toastVariants = cva( variant: "default", }, } -) +); const Toast = React.forwardRef< React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps + React.ComponentPropsWithoutRef & VariantProps >(({ className, variant, ...props }, ref) => { return ( - ) -}) -Toast.displayName = ToastPrimitives.Root.displayName + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; const ToastAction = React.forwardRef< React.ElementRef, @@ -65,8 +64,8 @@ const ToastAction = React.forwardRef< )} {...props} /> -)) -ToastAction.displayName = ToastPrimitives.Action.displayName +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; const ToastClose = React.forwardRef< React.ElementRef, @@ -78,25 +77,21 @@ const ToastClose = React.forwardRef< "absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600", className )} - toast-close="" + toast-close='' {...props} > - + -)) -ToastClose.displayName = ToastPrimitives.Close.displayName +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; const ToastTitle = React.forwardRef< React.ElementRef, React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => ( - -)) -ToastTitle.displayName = ToastPrimitives.Title.displayName + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef< React.ElementRef, @@ -107,21 +102,21 @@ const ToastDescription = React.forwardRef< className={cn("text-sm opacity-90", className)} {...props} /> -)) -ToastDescription.displayName = ToastPrimitives.Description.displayName +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; -type ToastProps = React.ComponentPropsWithoutRef +type ToastProps = React.ComponentPropsWithoutRef; -type ToastActionElement = React.ReactElement +type ToastActionElement = React.ReactElement; export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, Toast, - ToastTitle, - ToastDescription, - ToastClose, ToastAction, -} + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, + type ToastActionElement, + type ToastProps, +}; diff --git a/client/src/ui/toaster.tsx b/client/src/ui/toaster.tsx index 585f8c1..3542717 100644 --- a/client/src/ui/toaster.tsx +++ b/client/src/ui/toaster.tsx @@ -12,15 +12,13 @@ export function Toaster() { const { toasts } = useToast(); return ( - + {toasts.map(function ({ id, title, description, action, ...props }) { return ( -
- {title && {title}} - {description && ( - {description} - )} +
+ {title && º{title}} + {description && {description}}
{action} diff --git a/client/src/ui/use-toast.ts b/client/src/ui/use-toast.ts index eb35a83..a49b29c 100644 --- a/client/src/ui/use-toast.ts +++ b/client/src/ui/use-toast.ts @@ -1,76 +1,73 @@ // Inspired by react-hot-toast library -import * as React from "react" +import * as React from "react"; -import type { - ToastActionElement, - ToastProps, -} from "./toast" +import type { ToastActionElement, ToastProps } from "./toast"; -const TOAST_LIMIT = 1 -const TOAST_REMOVE_DELAY = 1000000 +const TOAST_LIMIT = 5; +const TOAST_REMOVE_DELAY = 10000; type ToasterToast = ToastProps & { - id: string - title?: React.ReactNode - description?: React.ReactNode - action?: ToastActionElement -} + id: string; + title?: React.ReactNode; + description?: React.ReactNode; + action?: ToastActionElement; +}; const actionTypes = { ADD_TOAST: "ADD_TOAST", UPDATE_TOAST: "UPDATE_TOAST", DISMISS_TOAST: "DISMISS_TOAST", REMOVE_TOAST: "REMOVE_TOAST", -} as const +} as const; -let count = 0 +let count = 0; function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER - return count.toString() + count = (count + 1) % Number.MAX_SAFE_INTEGER; + return count.toString(); } -type ActionType = typeof actionTypes +type ActionType = typeof actionTypes; type Action = | { - type: ActionType["ADD_TOAST"] - toast: ToasterToast + type: ActionType["ADD_TOAST"]; + toast: ToasterToast; } | { - type: ActionType["UPDATE_TOAST"] - toast: Partial + type: ActionType["UPDATE_TOAST"]; + toast: Partial; } | { - type: ActionType["DISMISS_TOAST"] - toastId?: ToasterToast["id"] + type: ActionType["DISMISS_TOAST"]; + toastId?: ToasterToast["id"]; } | { - type: ActionType["REMOVE_TOAST"] - toastId?: ToasterToast["id"] - } + type: ActionType["REMOVE_TOAST"]; + toastId?: ToasterToast["id"]; + }; interface State { - toasts: ToasterToast[] + toasts: ToasterToast[]; } -const toastTimeouts = new Map>() +const toastTimeouts = new Map>(); const addToRemoveQueue = (toastId: string) => { if (toastTimeouts.has(toastId)) { - return + return; } const timeout = setTimeout(() => { - toastTimeouts.delete(toastId) + toastTimeouts.delete(toastId); dispatch({ type: "REMOVE_TOAST", toastId: toastId, - }) - }, TOAST_REMOVE_DELAY) + }); + }, TOAST_REMOVE_DELAY); - toastTimeouts.set(toastId, timeout) -} + toastTimeouts.set(toastId, timeout); +}; export const reducer = (state: State, action: Action): State => { switch (action.type) { @@ -78,27 +75,25 @@ export const reducer = (state: State, action: Action): State => { return { ...state, toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - } + }; case "UPDATE_TOAST": return { ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t - ), - } + toasts: state.toasts.map((t) => (t.id === action.toast.id ? { ...t, ...action.toast } : t)), + }; case "DISMISS_TOAST": { - const { toastId } = action + const { toastId } = action; // ! Side effects ! - This could be extracted into a dismissToast() action, // but I'll keep it here for simplicity if (toastId) { - addToRemoveQueue(toastId) + addToRemoveQueue(toastId); } else { state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id) - }) + addToRemoveQueue(toast.id); + }); } return { @@ -111,44 +106,44 @@ export const reducer = (state: State, action: Action): State => { } : t ), - } + }; } case "REMOVE_TOAST": if (action.toastId === undefined) { return { ...state, toasts: [], - } + }; } return { ...state, toasts: state.toasts.filter((t) => t.id !== action.toastId), - } + }; } -} +}; -const listeners: Array<(state: State) => void> = [] +const listeners: Array<(state: State) => void> = []; -let memoryState: State = { toasts: [] } +let memoryState: State = { toasts: [] }; function dispatch(action: Action) { - memoryState = reducer(memoryState, action) + memoryState = reducer(memoryState, action); listeners.forEach((listener) => { - listener(memoryState) - }) + listener(memoryState); + }); } -type Toast = Omit +type Toast = Omit; function toast({ ...props }: Toast) { - const id = genId() + const id = genId(); const update = (props: ToasterToast) => dispatch({ type: "UPDATE_TOAST", toast: { ...props, id }, - }) - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + }); + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); dispatch({ type: "ADD_TOAST", @@ -157,37 +152,36 @@ function toast({ ...props }: Toast) { id, open: true, onOpenChange: (open) => { - if (!open) dismiss() + if (!open) dismiss(); }, }, - }) + }); return { id: id, dismiss, update, - } + }; } function useToast() { - const [state, setState] = React.useState(memoryState) + const [state, setState] = React.useState(memoryState); React.useEffect(() => { - listeners.push(setState) + listeners.push(setState); return () => { - const index = listeners.indexOf(setState) + const index = listeners.indexOf(setState); if (index > -1) { - listeners.splice(index, 1) + listeners.splice(index, 1); } - } - }, [state]) + }; + }, [state]); return { ...state, toast, dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - } + }; } -export { toast, useToast } - +export { toast, useToast };