From 79f26f876d034971dc443d1b9d5c768e41b70e84 Mon Sep 17 00:00:00 2001 From: mehmet sayin Date: Sun, 26 Nov 2023 14:07:55 +0100 Subject: [PATCH] feat: consistency in the pages --- client/package-lock.json | 68 +++++++ client/package.json | 2 + client/src/App.tsx | 1 - client/src/book-table/column.tsx | 10 +- client/src/components/BookPreview.tsx | 16 +- client/src/components/Layout.tsx | 26 +-- client/src/components/Navbar.tsx | 81 ++++----- client/src/components/Search.tsx | 11 +- client/src/components/Table.tsx | 215 ----------------------- client/src/components/ui/skeleton.tsx | 15 ++ client/src/components/ui/theme.ts | 2 +- client/src/components/ui/toast.tsx | 126 +++++++++++++ client/src/components/ui/toaster.tsx | 33 ++++ client/src/components/ui/use-toast.ts | 192 ++++++++++++++++++++ client/src/components/ui/user-avatar.tsx | 44 +++-- client/src/pages/BookPreviewPage.tsx | 21 ++- client/src/pages/Login.tsx | 8 +- client/src/pages/Register.tsx | 5 +- client/src/pages/ShelfSpace.tsx | 2 +- client/src/pages/UploadNewBook.tsx | 2 +- 20 files changed, 546 insertions(+), 334 deletions(-) delete mode 100644 client/src/components/Table.tsx create mode 100644 client/src/components/ui/skeleton.tsx create mode 100644 client/src/components/ui/toast.tsx create mode 100644 client/src/components/ui/toaster.tsx create mode 100644 client/src/components/ui/use-toast.ts diff --git a/client/package-lock.json b/client/package-lock.json index bc7b2f52..01f35602 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -19,6 +19,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@react-pdf/renderer": "^3.1.14", "@reduxjs/toolkit": "^1.8.3", "@tanstack/react-table": "^8.10.7", @@ -50,6 +51,7 @@ "react-table": "^7.8.0", "react-toastify": "^9.0.0", "reactstrap": "^9.1.3", + "sonner": "^1.2.3", "styled-components": "^6.0.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", @@ -5253,6 +5255,40 @@ } } }, + "node_modules/@radix-ui/react-toast": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", + "integrity": "sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-collection": "1.0.3", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-dismissable-layer": "1.0.5", + "@radix-ui/react-portal": "1.0.4", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-use-callback-ref": "1.0.1", + "@radix-ui/react-use-controllable-state": "1.0.1", + "@radix-ui/react-use-layout-effect": "1.0.1", + "@radix-ui/react-visually-hidden": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz", @@ -5359,6 +5395,29 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz", + "integrity": "sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.0.1.tgz", @@ -23361,6 +23420,15 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/sonner": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.2.3.tgz", + "integrity": "sha512-LMr155izOFA8hudzuUVQT0H93VqmcF9ODP475YjjC/4INESYWN1/ioC5SYRG20jmDmwuQDR8ugP7y6ELghT6JQ==", + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, "node_modules/source-list-map": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", diff --git a/client/package.json b/client/package.json index a5388d83..4f9c80d5 100644 --- a/client/package.json +++ b/client/package.json @@ -14,6 +14,7 @@ "@radix-ui/react-icons": "^1.3.0", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.5", "@react-pdf/renderer": "^3.1.14", "@reduxjs/toolkit": "^1.8.3", "@tanstack/react-table": "^8.10.7", @@ -45,6 +46,7 @@ "react-table": "^7.8.0", "react-toastify": "^9.0.0", "reactstrap": "^9.1.3", + "sonner": "^1.2.3", "styled-components": "^6.0.0", "tailwind-merge": "^2.0.0", "tailwindcss-animate": "^1.0.7", diff --git a/client/src/App.tsx b/client/src/App.tsx index be886aa4..fdf614f8 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -1,4 +1,3 @@ -import 'react-toastify/dist/ReactToastify.css'; import { Search } from './components/Search'; import 'bootstrap/dist/css/bootstrap.min.css'; import Layout from './components/Layout'; diff --git a/client/src/book-table/column.tsx b/client/src/book-table/column.tsx index 75f8e546..93e5e489 100644 --- a/client/src/book-table/column.tsx +++ b/client/src/book-table/column.tsx @@ -85,10 +85,10 @@ export const columns: ColumnDef[] = [ }, { accessorKey: 'date', - header: 'Date', + header: () =>
Date
, cell: ({ row }) => { return ( -
+
{new Date(row.original.date).toLocaleDateString()}
); @@ -96,10 +96,12 @@ export const columns: ColumnDef[] = [ }, { - header: 'Uploaded By', + header: () =>
Uploader
, accessorKey: 'uploader', cell: ({ row }) => ( -
{row.original.uploader.username}
+
+ {row.original.uploader.username} +
), }, { diff --git a/client/src/components/BookPreview.tsx b/client/src/components/BookPreview.tsx index abc84bb7..b4bb23d9 100644 --- a/client/src/components/BookPreview.tsx +++ b/client/src/components/BookPreview.tsx @@ -26,7 +26,12 @@ export const BookPreview = ({ if (fileType === 'pdf') { return ( -
+ <> + navigate(-1)} + /> -
+ ); } else if (fileType === 'epub') { return ( -
- navigate(-1)} - /> +
(); + const dispatch = useDispatch>(); useEffect(() => { dispatch(loadUserThunk()); }, [dispatch]); return ( - - + +
-
{children}
- +
{children}
+
); } diff --git a/client/src/components/Navbar.tsx b/client/src/components/Navbar.tsx index a2f66222..4b7f6596 100644 --- a/client/src/components/Navbar.tsx +++ b/client/src/components/Navbar.tsx @@ -10,53 +10,42 @@ export default function NavbarComponent() { ); return ( -
- - + + Flowbite React Logo + + + + + Home + + - Flowbite React Logo - - - - - Home - - - Recently Added - - - Upload Book - - - All Books - - - - -
+ Recently Added + + + Upload Book + + + All Books + + + + ); } diff --git a/client/src/components/Search.tsx b/client/src/components/Search.tsx index c52ebb5e..18a8d633 100644 --- a/client/src/components/Search.tsx +++ b/client/src/components/Search.tsx @@ -10,6 +10,7 @@ import { columns } from '../book-table/column'; import { Button } from './ui/button'; import { Input } from './ui/input'; import { Form, FormControl, FormField, FormItem } from './ui/form'; +import { Spinner } from 'flowbite-react'; const formSchema = z.object({ name: z.string().min(2).max(50), @@ -58,7 +59,11 @@ export const Search = () => { ]); if (isLoading) { - return
Loading...
; + return ( +
+ +
+ ); } if (isError) { @@ -89,14 +94,14 @@ export const Search = () => { Search for a book example: 'George Orwell' " {...field} - className="w-96" + className="w-80" /> )} /> {!isLoading && ( - )} diff --git a/client/src/components/Table.tsx b/client/src/components/Table.tsx deleted file mode 100644 index 96d79ea6..00000000 --- a/client/src/components/Table.tsx +++ /dev/null @@ -1,215 +0,0 @@ -import React, { useEffect, useMemo } from 'react'; -import { useSelector } from 'react-redux'; -import { useTable, useSortBy, usePagination, Column } from 'react-table'; -import { toast } from 'react-toastify'; -import { AiOutlineDelete } from 'react-icons/ai'; -import Loading from './Loading'; -import { - Book, - BooksData, - useDeleteBookMutation, -} from '../redux/services/book.api'; -import { Button } from './ui/button'; - -type TableTypes = { - books: BooksData; - setPage: (page: number) => void; - isLoading: boolean; -}; - -type ColumnWithShow> = Column & { - show?: boolean; -}; - -export const Table = ({ books, setPage, isLoading }: TableTypes) => { - const [data, setData] = React.useState(books.results); - const [deleteBook] = useDeleteBookMutation(); - - useEffect(() => { - setData(books.results); - }, [books]); - - const { next, previous } = books; - - const { user: USERINFO, isLoggedIn } = useSelector( - (state: any) => state.authSlice - ); - - const isAdmin = isLoggedIn && USERINFO.user?.isAdmin; - - console.log(isAdmin); - - const columns = useMemo( - (): readonly ColumnWithShow[] => [ - { - Header: 'Name', - id: 'name', - show: true, - accessor: (d: Book) => d.file, - Cell: ({ row }: any) => { - return ( - - {row.original.name} - - ); - }, - }, - { - Header: 'Size', - accessor: 'size', // accessor is the "key" in the data - }, - - { - Header: 'Date', - accessor: (d: Book) => d.date, - Cell: ({ row }: any) => { - return ( -
- {new Date(row.original.date).toLocaleDateString()} -
- ); - }, - }, - { - Header: 'Uploaded By', - accessor: 'uploader', - Cell: ({ row }) => ( -
- {row.original.uploader.username} -
- ), - }, - { - Header: 'Category', - accessor: 'category', - id: 'category', - show: isAdmin, - Cell: ({ row }) => ( -
{row.original?.category[0]}
- ), - }, - { - Header: 'Language', - accessor: 'language', - id: 'language', - show: isAdmin, - Cell: ({ row }) => ( -
{row.original.language}
- ), - }, - { - Header: 'Delete', - id: 'delete', - show: isAdmin, - Cell: ({ row }: any) => ( -
- { - return deleteBook(row.original._id) - .unwrap() - .then(() => { - toast.success('Book deleted successfully'); - }); - }} - /> -
- ), - }, - ], - [deleteBook, isAdmin] - ); - - const { - getTableProps, - getTableBodyProps, - headerGroups, - prepareRow, - page, - setHiddenColumns, - } = useTable( - { - columns, - data, - }, - useSortBy, - usePagination - ) as any; - - React.useEffect(() => { - setHiddenColumns( - columns.filter((column) => !column.show).map((column) => column.id) - ); - }, [columns, setHiddenColumns]); - - if (isLoading) { - return ; - } - - return ( -
- - - {headerGroups.map((headerGroup: any) => ( - - {headerGroup.headers.map((column: any) => ( - - ))} - - ))} - - - {page.map((row: any) => { - prepareRow(row); - return ( - - {row.cells.map((cell: any) => { - return ( - - ); - })} - - ); - })} - -
- {column.render('Header')} - - {column.isSorted ? (column.isSortedDesc ? '🔽' : '🔼') : ''} - -
- {cell.render('Cell')} -
-
- - -
-
- ); -}; diff --git a/client/src/components/ui/skeleton.tsx b/client/src/components/ui/skeleton.tsx new file mode 100644 index 00000000..98005341 --- /dev/null +++ b/client/src/components/ui/skeleton.tsx @@ -0,0 +1,15 @@ +import { cn } from '../../lib/utils'; + +function Skeleton({ + className, + ...props +}: React.HTMLAttributes) { + return ( +
+ ); +} + +export { Skeleton }; diff --git a/client/src/components/ui/theme.ts b/client/src/components/ui/theme.ts index 453c37ba..77ffa2c9 100644 --- a/client/src/components/ui/theme.ts +++ b/client/src/components/ui/theme.ts @@ -35,7 +35,7 @@ export const customTheme: CustomFlowbiteTheme = { base: 'block py-2 pr-4 pl-3 md:p-0', active: { on: 'bg-cyan-700 text-white dark:text-white md:bg-transparent md:text-cyan-700', - off: 'border-b border-gray-100 text-gray-700 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white md:border-0 md:hover:bg-transparent md:hover:text-cyan-700 md:dark:hover:bg-transparent md:dark:hover:text-white', + off: 'border-b border-gray-100 text-gray-400 hover:bg-gray-50 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white md:border-0 md:hover:bg-transparent md:hover:text-cyan-700 md:dark:hover:bg-transparent md:dark:hover:text-white', }, disabled: { on: 'text-gray-400 hover:cursor-not-allowed dark:text-gray-600', diff --git a/client/src/components/ui/toast.tsx b/client/src/components/ui/toast.tsx new file mode 100644 index 00000000..75d0c5cd --- /dev/null +++ b/client/src/components/ui/toast.tsx @@ -0,0 +1,126 @@ +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 { cn } from '../../lib/utils'; + +const ToastProvider = ToastPrimitives.Provider; + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +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', + { + variants: { + variant: { + default: 'border bg-background text-foreground', + destructive: + 'destructive group border-destructive bg-destructive text-destructive-foreground', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +); + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ); +}); +Toast.displayName = ToastPrimitives.Root.displayName; + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastAction.displayName = ToastPrimitives.Action.displayName; + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)); +ToastClose.displayName = ToastPrimitives.Close.displayName; + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastTitle.displayName = ToastPrimitives.Title.displayName; + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +ToastDescription.displayName = ToastPrimitives.Description.displayName; + +type ToastProps = React.ComponentPropsWithoutRef; + +type ToastActionElement = React.ReactElement; + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +}; diff --git a/client/src/components/ui/toaster.tsx b/client/src/components/ui/toaster.tsx new file mode 100644 index 00000000..77f9c06f --- /dev/null +++ b/client/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from './toast'; +import { useToast } from '@/components/ui/use-toast'; + +export function Toaster() { + const { toasts } = useToast(); + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ); + })} + +
+ ); +} diff --git a/client/src/components/ui/use-toast.ts b/client/src/components/ui/use-toast.ts new file mode 100644 index 00000000..16713070 --- /dev/null +++ b/client/src/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + 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 + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_SAFE_INTEGER + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + 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 + ), + } + + case "DISMISS_TOAST": { + 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) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : 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> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast } diff --git a/client/src/components/ui/user-avatar.tsx b/client/src/components/ui/user-avatar.tsx index 018b4535..201e53ec 100644 --- a/client/src/components/ui/user-avatar.tsx +++ b/client/src/components/ui/user-avatar.tsx @@ -8,12 +8,12 @@ import { DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, - DropdownMenuShortcut, DropdownMenuTrigger, } from './dropdown-menu'; import { Dispatch } from '@reduxjs/toolkit'; import { logoutThunk } from '../../redux/authSlice'; -import { Link } from 'react-router-dom'; +import { Link, useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; export interface UserNavProps { avatarUrl?: string; @@ -27,28 +27,32 @@ export function UserNav({ email = 'exampe@example.com', }: UserNavProps) { const dispatch = useDispatch>(); + const navigate = useNavigate(); const handleLogout = () => { dispatch(logoutThunk()); + toast.success('Logout successful'); + }; + + const handleLogin = () => { + navigate('/login'); }; return ( - + + + + {username ? ( + username.slice(0, 2).toUpperCase() + ) : ( + + )} + + @@ -62,11 +66,15 @@ export function UserNav({ - Profile + + {username ? Profile : Register} + - Log out + + {username ? Logout : Login} + ); diff --git a/client/src/pages/BookPreviewPage.tsx b/client/src/pages/BookPreviewPage.tsx index f1f4ae09..bd0237e4 100644 --- a/client/src/pages/BookPreviewPage.tsx +++ b/client/src/pages/BookPreviewPage.tsx @@ -3,6 +3,7 @@ import { BookPreview } from '../components/BookPreview'; import { useGetBookByIdQuery } from '../redux/services/book.api'; import { FC } from 'react'; import { useParams } from 'react-router-dom'; +import { Spinner } from 'flowbite-react'; export const BookPreviewPage: FC = () => { const router = useParams<{ bookId: string }>(); @@ -12,21 +13,23 @@ export const BookPreviewPage: FC = () => { const fileType = book?.url ? book.url.split('.').pop() : ''; - if (isLoading || !book) { - return
Loading...
; - } - if (isError) { return
Error...
; } return ( - + {isLoading ? ( +
+ +
+ ) : ( + + )}
); }; diff --git a/client/src/pages/Login.tsx b/client/src/pages/Login.tsx index 83461696..1077df5b 100644 --- a/client/src/pages/Login.tsx +++ b/client/src/pages/Login.tsx @@ -1,11 +1,9 @@ -import React, { SyntheticEvent } from 'react'; +import { SyntheticEvent } from 'react'; import { useDispatch, useSelector } from 'react-redux'; import { Link } from 'react-router-dom'; +import { toast } from 'sonner'; import styled from 'styled-components'; import Layout from '../components/Layout'; -import { ToastContainer, toast } from 'react-toastify'; - -import 'react-toastify/dist/ReactToastify.css'; import { loginThunk } from '../redux/authSlice'; import { mobile } from '../responsive'; import User from '../components/User'; @@ -80,6 +78,7 @@ export default function Login() { const { username, password } = Object.fromEntries(data.entries()); try { dispatch(loginThunk({ username, password })); + toast.success('Login successful'); } catch (error) {} }; @@ -119,7 +118,6 @@ export default function Login() {
)} - ); } diff --git a/client/src/pages/Register.tsx b/client/src/pages/Register.tsx index 5fd95499..f0ba8b5c 100644 --- a/client/src/pages/Register.tsx +++ b/client/src/pages/Register.tsx @@ -3,8 +3,7 @@ import { mobile } from '../responsive'; import { useDispatch } from 'react-redux'; import { useNavigate } from 'react-router-dom'; import { registerThunk } from '../redux/authSlice'; -import { ToastContainer, toast } from 'react-toastify'; -import 'react-toastify/dist/ReactToastify.css'; +import { toast } from 'sonner'; import Layout from '../components/Layout'; import React, { SyntheticEvent } from 'react'; @@ -98,8 +97,6 @@ const Register = () => { return ( - - CREATE AN ACCOUNT
diff --git a/client/src/pages/ShelfSpace.tsx b/client/src/pages/ShelfSpace.tsx index e0948829..2c227c04 100644 --- a/client/src/pages/ShelfSpace.tsx +++ b/client/src/pages/ShelfSpace.tsx @@ -5,7 +5,7 @@ import { useSelector } from 'react-redux'; import { Button, Form, FormGroup, Input } from 'reactstrap'; import Layout from '../components/Layout'; import Loading from '../components/Loading'; -import { toast } from 'react-toastify'; +import { toast } from 'sonner'; import { useAddMessageMutation, useDeleteMessageMutation, diff --git a/client/src/pages/UploadNewBook.tsx b/client/src/pages/UploadNewBook.tsx index b4c5c1ef..ca8caf0d 100644 --- a/client/src/pages/UploadNewBook.tsx +++ b/client/src/pages/UploadNewBook.tsx @@ -1,4 +1,4 @@ -import { toast } from 'react-toastify'; +import { toast } from 'sonner'; import 'react-dropzone-uploader/dist/styles.css'; import Dropzone from 'react-dropzone-uploader';