diff --git a/.env.example b/.env.example index fa8ac5ee..ad8971f6 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ VITE_API_URL=http://localhost:4000 -VITE_HMAC_SECRET_KEY= \ No newline at end of file +VITE_HMAC_SECRET_KEY= +VITE_REQUEST_CACHE=false \ No newline at end of file diff --git a/src/App.tsx b/src/App.tsx index 478da36d..af7fd636 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,8 +2,9 @@ import { Fragment } from 'react'; import { BrowserRouter } from 'react-router-dom'; import { Routes } from './routes/Routes'; -import { SessionProvider } from './contexts'; +import { DonationCartProvider, SessionProvider } from './contexts'; import { Toaster } from './components/ui/toaster'; +import { BackToTop } from '@/components'; const App = () => { return ( @@ -11,7 +12,10 @@ const App = () => { - + + + + diff --git a/src/components/BackToTop/BackToTop.tsx b/src/components/BackToTop/BackToTop.tsx new file mode 100644 index 00000000..e1c418de --- /dev/null +++ b/src/components/BackToTop/BackToTop.tsx @@ -0,0 +1,45 @@ +import { useState } from 'react'; +import { ArrowUp } from 'lucide-react'; + +const BackToTop = () => { + const [isVisible, setVisibility] = useState(false); + + const scrollToTop = () => { + const root = document.getElementById('root'); + if (!root) { + return; + } + + root.scrollTo({ top: 0, behavior: 'smooth' }); + }; + + document.getElementById('root')?.addEventListener('scroll', (e) => { + if (e.target === null) { + return; + } + const CurrentScrollHeight = (e.target as HTMLElement).scrollTop; + const WindowHeight = window.innerHeight; + + if (CurrentScrollHeight > WindowHeight / 2) { + setVisibility(true); + } else { + setVisibility(false); + } + }); + + return ( + isVisible && ( + + ) + ); +}; + +export { BackToTop }; diff --git a/src/components/BackToTop/index.ts b/src/components/BackToTop/index.ts new file mode 100644 index 00000000..2e570724 --- /dev/null +++ b/src/components/BackToTop/index.ts @@ -0,0 +1,3 @@ +import { BackToTop } from "./BackToTop"; + +export { BackToTop }; diff --git a/src/components/BurgerMenu/BurgerMenu.tsx b/src/components/BurgerMenu/BurgerMenu.tsx index 2f823e5b..db04d613 100644 --- a/src/components/BurgerMenu/BurgerMenu.tsx +++ b/src/components/BurgerMenu/BurgerMenu.tsx @@ -8,6 +8,7 @@ import { LinkIcon, Menu, ShieldAlert, + X, } from 'lucide-react'; import { SessionServices } from '@/service'; @@ -16,6 +17,9 @@ import { BurguerMenuItem } from './components'; import { Separator } from '../ui/separator'; import { SessionContext } from '@/contexts'; import { usePartners } from '@/hooks'; +import { DialogClose } from '@radix-ui/react-dialog'; +import { Button } from '../ui/button'; +import { DialogFooter } from '../ui/dialog'; const BurgerMenu = () => { const { session } = useContext(SessionContext); @@ -37,7 +41,14 @@ const BurgerMenu = () => { - + + + + + +
{session && ( diff --git a/src/components/Chip/Chip.tsx b/src/components/Chip/Chip.tsx index 6440a5c3..af3e3777 100644 --- a/src/components/Chip/Chip.tsx +++ b/src/components/Chip/Chip.tsx @@ -5,23 +5,32 @@ import { cva } from 'class-variance-authority'; const Chip = React.forwardRef((props, ref) => { const { label, className, variant = 'info', ...rest } = props; - const variants = cva('px-4 py-1.5 font-medium text-sm md:text-md rounded-2xl', { - variants: { - variant: { - warn: 'bg-light-yellow', - success: 'bg-light-green', - danger: 'bg-light-red', - alert: 'bg-light-orange', - info: 'bg-light-blue', + const variants = cva( + 'px-4 py-1.5 font-medium text-sm md:text-md rounded-2xl', + { + variants: { + variant: { + warn: 'bg-light-yellow', + success: 'bg-light-green', + danger: 'bg-light-red', + alert: 'bg-light-orange', + info: 'bg-light-blue', + moreInfo: 'bg-gray-200 text-black-600', + }, }, - }, - defaultVariants: { - variant: 'info', - }, - }); + defaultVariants: { + variant: 'info', + }, + } + ); return ( - + {label} ); diff --git a/src/components/Chip/types.ts b/src/components/Chip/types.ts index 22bfb972..f660a809 100644 --- a/src/components/Chip/types.ts +++ b/src/components/Chip/types.ts @@ -1,4 +1,4 @@ -export type ChipVariant = 'info' | 'success' | 'warn' | 'danger'; +export type ChipVariant = 'info' | 'success' | 'warn' | 'danger' | 'moreInfo'; export interface IChipProps extends React.ComponentPropsWithoutRef<'div'> { label: string; diff --git a/src/components/DonationCart/DonationCart.tsx b/src/components/DonationCart/DonationCart.tsx new file mode 100644 index 00000000..5fed55bd --- /dev/null +++ b/src/components/DonationCart/DonationCart.tsx @@ -0,0 +1,45 @@ +import { useEffect, useState } from 'react'; + +import { IDonationCart } from './types'; +import { Sheet, SheetContent } from '../ui/sheet'; +import { DonationCartForm, DonationSuccess } from './components'; + +const DonationCart = (props: IDonationCart) => { + const { onClose, opened, shelterId } = props; + const [donationOrderId, setDonationOrderId] = useState(null); + + useEffect(() => { + const el = document.querySelector('header'); + if (el) { + if (opened) { + el?.classList.remove('z-[100]'); + el?.classList.add('z-0'); + } else { + el?.classList.remove('z-0'); + el?.classList.add('z-[100]'); + } + } + }, [opened]); + + useEffect(() => { + if (!opened) setDonationOrderId(null); + }, [opened]); + + return ( + + + {donationOrderId ? ( + + ) : ( + setDonationOrderId(orderId)} + /> + )} + + + ); +}; + +export { DonationCart }; diff --git a/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx b/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx new file mode 100644 index 00000000..98b90f80 --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/DonationCartForm.tsx @@ -0,0 +1,273 @@ +import React, { useCallback, useContext, useMemo, useState } from 'react'; +import { Trash2 } from 'lucide-react'; + +import { + SheetDescription, + SheetFooter, + SheetHeader, + SheetTitle, +} from '../../../ui/sheet'; +import { DonationCartContext, SessionContext } from '@/contexts'; +import { Input } from '../../../ui/input'; +import { Button } from '../../../ui/button'; +import { SupplyMeasureMap, cn, getSupplyPriorityProps } from '@/lib/utils'; +import { CircleStatus } from '../../../CircleStatus'; +import { Separator } from '../../../ui/separator'; +import { IDonationCartItem } from '@/contexts/DonationCartContext/types'; +import { + DonationOrderServices, + SessionServices, + // ShelterSupplyServices, + UserServices, +} from '@/service'; +import { IDonateItem } from '@/service/donationOrder/types'; +import { TextField } from '../../../TextField'; +import { ICreateUser } from '@/service/users/types'; +import { IDonationCartForm } from './types'; + +const DonationCartForm = React.forwardRef( + (props, ref) => { + const { shelterId, onCancel, onSuccess, className = '', ...rest } = props; + const { refreshSession, session } = useContext(SessionContext); + const { carts, removeItem, clearCart, updateItem } = + useContext(DonationCartContext); + const [loading, setLoading] = useState(false); + const cart = useMemo(() => carts[shelterId] ?? [], [carts, shelterId]); + const [errors, setErrors] = useState>({}); + const [values, setValues] = useState>({}); + + const handleCancelCart = useCallback(() => { + clearCart(shelterId); + if (onCancel) onCancel(); + }, [clearCart, onCancel, shelterId]); + + const handleChangeQuantity = useCallback( + (item: IDonationCartItem, quantity: number) => { + setValues((prev) => ({ + ...prev, + [item.id]: quantity, + })); + updateItem(shelterId, item.id, { quantity }); + }, + [shelterId, updateItem] + ); + + // const verifyCartItems = useCallback( + // async ( + // shelterId: string, + // items: IDonateItem[] + // ): Promise> => { + // const { data } = await ShelterSupplyServices.getAll(shelterId); + // const newErrors = items.reduce((prev, current) => { + // const finded = data.find((d) => d.supply.id === current.id); + // const ok = current.quantity <= (finded?.quantity ?? 0); + // if (ok || !finded) return prev; + // else { + // const measure = SupplyMeasureMap[finded.supply.measure]; + // return { + // ...prev, + // [current.id]: `A doação de ${finded.supply.name} não pode ser maior que a quantidade máxima de ${finded.quantity}${measure} `, + // }; + // } + // }, {}); + // return newErrors; + // }, + // [] + // ); + + const verifyAccountExists = useCallback(async (phone: string) => { + const { data } = await UserServices.find('phone', phone); + if (data.exists) { + setErrors({ + phone: + 'Já existe um usuário com este telefone. Faça login ou tente outro telefone', + }); + return false; + } + return true; + }, []); + + const handleCreateAccount = useCallback( + async (payload: Record) => { + const { lastName, name, phone } = payload; + if (name && lastName && phone) { + const ok = await verifyAccountExists(phone.toString()); + if (!ok) return false; + + await UserServices.create({ + phone: phone.toString(), + name: name.toString(), + lastName: lastName.toString(), + }); + const parsedPhone = phone.toString().replace(/[^0-9]/g, ''); + const resp = await SessionServices.auth({ + login: parsedPhone, + password: parsedPhone, + }); + localStorage.setItem('token', resp.token); + refreshSession(); + } + + return true; + }, + [refreshSession, verifyAccountExists] + ); + + const handleDonate = useCallback( + async (ev: React.FormEvent) => { + ev.preventDefault(); + + setLoading(true); + try { + const form = new FormData(ev.currentTarget); + const formData = Object.fromEntries(form); + const { name, lastName, phone, ...rest } = formData; + const ok = await handleCreateAccount({ name, lastName, phone }); + if (ok) { + const items = Object.entries(rest).reduce( + (prev, [key, value]) => [...prev, { id: key, quantity: +value }], + [] as IDonateItem[] + ); + //TODO: discutir produto se vai e como será verificado os "erros" do carrinho + // const errorsData = await verifyCartItems(shelterId, items); + // setErrors(errorsData); + // if (Object.keys(errorsData).length === 0) { + const resp = await DonationOrderServices.store({ + shelterId, + supplies: items, + }); + if (onSuccess) onSuccess(resp.data.id); + clearCart(shelterId); + // } + } + } catch (err) { + console.log('Ocorreu um erro ao realizar a doação'); + } finally { + setLoading(false); + } + }, + [clearCart, handleCreateAccount, onSuccess, shelterId] + ); + + return ( +
+ + + {[session?.name, 'Revise sua doação'].filter((p) => !!p).join(', ')} + + + Ajuste a quantidade que gostaria de doar em cada item + + +
+
+ Item + Quantidade +
+ {cart.map((item) => { + const { className } = getSupplyPriorityProps(item.priority); + return ( +
+
+
+ + {item.name} +
+
+
+ + handleChangeQuantity(item, +ev.target.value) + } + /> + + {SupplyMeasureMap[item.measure]} + +
+ +
+
+ {errors[item.id] && ( + +

{errors[item.id]}

+
+ )} +
+ ); + })} + + {!session && ( +
+

Comunique sua doação

+

+ Utilizaremos seus dados apenas para comunicar ao abrigo que sua + doação esta a caminho. +

+
+ + + +
+
+ )} +
+ +
+ + +
+
+
+ ); + } +); + +export { DonationCartForm }; diff --git a/src/components/DonationCart/components/DonationCartForm/index.ts b/src/components/DonationCart/components/DonationCartForm/index.ts new file mode 100644 index 00000000..f07b238a --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/index.ts @@ -0,0 +1,3 @@ +import { DonationCartForm } from './DonationCartForm'; + +export { DonationCartForm }; diff --git a/src/components/DonationCart/components/DonationCartForm/types.ts b/src/components/DonationCart/components/DonationCartForm/types.ts new file mode 100644 index 00000000..85a28027 --- /dev/null +++ b/src/components/DonationCart/components/DonationCartForm/types.ts @@ -0,0 +1,5 @@ +export interface IDonationCartForm extends React.ComponentPropsWithRef<'form'> { + shelterId: string; + onCancel?: () => void; + onSuccess?: (donationOrderId: string) => void; +} diff --git a/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx new file mode 100644 index 00000000..6418d37e --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/DonationSuccess.tsx @@ -0,0 +1,83 @@ +import React from 'react'; +import { Circle, HeartHandshake, Loader, Printer } from 'lucide-react'; + +import { + SheetHeader, + SheetTitle, + SheetDescription, +} from '@/components/ui/sheet'; +import { IDonationSuccessProps } from './types'; +import { SupplyMeasureMap, cn } from '@/lib/utils'; +import { useDonationOrder } from '@/hooks'; +import { format } from 'date-fns'; +import { Button } from '@/components/ui/button'; + +const DonationSuccess = React.forwardRef( + (props, ref) => { + const { donationOrderId, className = '', ...rest } = props; + const { data: donation, loading } = useDonationOrder(donationOrderId); + + if (loading) + return ; + + return ( +
+ + O RS agradece sua doação + + + + +
+
+ + Cada doação importa. + + + Juntos somos mais fortes! + +
+
+
+ + Doação para + +

{donation.shelter.name}

+ + às{' '} + {format(new Date(donation.createdAt), "HH'h'mm 'de' dd/MM/yy")} + +
+
    + {donation.donationOrderSupplies.map((s, idx) => ( +
  • + + {s.quantity} + {SupplyMeasureMap[s.supply.measure]} de {s.supply.name} +
  • + ))} +
+ +
+
+
+ +
+
+ ); + } +); + +export { DonationSuccess }; diff --git a/src/components/DonationCart/components/DonationSuccess/index.ts b/src/components/DonationCart/components/DonationSuccess/index.ts new file mode 100644 index 00000000..aef6420e --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/index.ts @@ -0,0 +1,3 @@ +import { DonationSuccess } from './DonationSuccess'; + +export { DonationSuccess }; diff --git a/src/components/DonationCart/components/DonationSuccess/types.ts b/src/components/DonationCart/components/DonationSuccess/types.ts new file mode 100644 index 00000000..12de8593 --- /dev/null +++ b/src/components/DonationCart/components/DonationSuccess/types.ts @@ -0,0 +1,4 @@ +export interface IDonationSuccessProps + extends React.ComponentPropsWithoutRef<'div'> { + donationOrderId: string; +} diff --git a/src/components/DonationCart/components/index.ts b/src/components/DonationCart/components/index.ts new file mode 100644 index 00000000..ffced286 --- /dev/null +++ b/src/components/DonationCart/components/index.ts @@ -0,0 +1,4 @@ +import { DonationCartForm } from './DonationCartForm'; +import { DonationSuccess } from './DonationSuccess'; + +export { DonationCartForm, DonationSuccess }; diff --git a/src/components/DonationCart/index.ts b/src/components/DonationCart/index.ts new file mode 100644 index 00000000..0013386e --- /dev/null +++ b/src/components/DonationCart/index.ts @@ -0,0 +1,3 @@ +import { DonationCart } from './DonationCart'; + +export { DonationCart }; diff --git a/src/components/DonationCart/types.ts b/src/components/DonationCart/types.ts new file mode 100644 index 00000000..bdd8708b --- /dev/null +++ b/src/components/DonationCart/types.ts @@ -0,0 +1,5 @@ +export interface IDonationCart { + shelterId: string; + opened: boolean; + onClose: () => void; +} diff --git a/src/components/DonationCartIcon/DonationCartIcon.tsx b/src/components/DonationCartIcon/DonationCartIcon.tsx new file mode 100644 index 00000000..478e7959 --- /dev/null +++ b/src/components/DonationCartIcon/DonationCartIcon.tsx @@ -0,0 +1,29 @@ +import { useContext } from 'react'; +import { HandHeart } from 'lucide-react'; + +import { Button } from '../ui/button'; +import { DonationCartContext } from '@/contexts'; +import { IDonationCartIconProps } from './types'; + +const DonationCartIcon = (props: IDonationCartIconProps) => { + const { quantity } = props; + const { toggleOpened } = useContext(DonationCartContext); + + return ( + + ); +}; + +export { DonationCartIcon }; diff --git a/src/components/DonationCartIcon/index.ts b/src/components/DonationCartIcon/index.ts new file mode 100644 index 00000000..7c7ffa6b --- /dev/null +++ b/src/components/DonationCartIcon/index.ts @@ -0,0 +1,3 @@ +import { DonationCartIcon } from './DonationCartIcon'; + +export { DonationCartIcon }; diff --git a/src/components/DonationCartIcon/types.ts b/src/components/DonationCartIcon/types.ts new file mode 100644 index 00000000..eee4304e --- /dev/null +++ b/src/components/DonationCartIcon/types.ts @@ -0,0 +1,3 @@ +export interface IDonationCartIconProps { + quantity?: number; +} diff --git a/src/components/Header/Header.tsx b/src/components/Header/Header.tsx index 9dde1265..b3255f6d 100644 --- a/src/components/Header/Header.tsx +++ b/src/components/Header/Header.tsx @@ -1,8 +1,8 @@ import React from 'react'; +import { Link } from 'react-router-dom'; import { IHeader } from './types'; import { cn } from '@/lib/utils'; -import { Link } from 'react-router-dom'; const Header = React.forwardRef((props, ref) => { const { @@ -14,10 +14,10 @@ const Header = React.forwardRef((props, ref) => { } = props; return ( -
((props, ref) => { {title}
-
-
{endAdornment}
-
-
+
{endAdornment}
+ ); }); diff --git a/src/components/SearchInput/SearchInput.tsx b/src/components/SearchInput/SearchInput.tsx index 225ddb87..cbd723d2 100644 --- a/src/components/SearchInput/SearchInput.tsx +++ b/src/components/SearchInput/SearchInput.tsx @@ -7,21 +7,26 @@ import { cn } from '@/lib/utils'; const SearchInput = React.forwardRef( (props, ref) => { + const { inputProps, value, onChange, className, ...rest } = props; const { - value, - onChange, - className, placeholder = 'Buscar por abrigo ou endereço', - ...rest - } = props; + className: inputClassName = '', + ...restInputProps + } = inputProps ?? {}; return (
+ onChange ? onChange(ev.target.value ?? '') : undefined + } + {...restInputProps} />
diff --git a/src/components/SearchInput/types.ts b/src/components/SearchInput/types.ts index b15517df..9594490e 100644 --- a/src/components/SearchInput/types.ts +++ b/src/components/SearchInput/types.ts @@ -1,4 +1,9 @@ export interface ISearchInputProps - extends React.ComponentPropsWithoutRef<'input'> { + extends Omit, 'onChange'> { value: string; + onChange?: (value: string) => void; + inputProps?: Omit< + React.ComponentPropsWithoutRef<'input'>, + 'value' | 'onChange' + >; } diff --git a/src/components/TextField/TextField.tsx b/src/components/TextField/TextField.tsx index db4fa822..43a2adf0 100644 --- a/src/components/TextField/TextField.tsx +++ b/src/components/TextField/TextField.tsx @@ -14,7 +14,7 @@ const TextField = forwardRef, TextFieldProps>( className, error, helperText, - value = '', + value, ...rest } = props; diff --git a/src/components/index.ts b/src/components/index.ts index 542ce455..fe877999 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -14,6 +14,9 @@ import { VerifiedBadge } from './VerifiedBadge'; import { SelectField } from './SelectField'; import { Authenticated } from './Authenticated'; import { BurgerMenu } from './BurgerMenu'; +import { BackToTop } from './BackToTop'; +import { DonationCartIcon } from './DonationCartIcon'; +import { DonationCart } from './DonationCart'; export { LoadingScreen, @@ -32,4 +35,7 @@ export { SelectField, Authenticated, BurgerMenu, + BackToTop, + DonationCartIcon, + DonationCart, }; diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 117f9957..d64a6fa7 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -1,17 +1,17 @@ -import * as React from 'react'; -import * as SheetPrimitive from '@radix-ui/react-dialog'; -import { cva, type VariantProps } from 'class-variance-authority'; +import * as React from "react" +import * as SheetPrimitive from "@radix-ui/react-dialog" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" -import { cn } from '@/lib/utils'; -import { X } from 'lucide-react'; +import { cn } from "@/lib/utils" -const Sheet = SheetPrimitive.Root; +const Sheet = SheetPrimitive.Root -const SheetTrigger = SheetPrimitive.Trigger; +const SheetTrigger = SheetPrimitive.Trigger -const SheetClose = SheetPrimitive.Close; +const SheetClose = SheetPrimitive.Close -const SheetPortal = SheetPrimitive.Portal; +const SheetPortal = SheetPrimitive.Portal const SheetOverlay = React.forwardRef< React.ElementRef, @@ -19,33 +19,33 @@ const SheetOverlay = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetOverlay.displayName = SheetPrimitive.Overlay.displayName; +)) +SheetOverlay.displayName = SheetPrimitive.Overlay.displayName const sheetVariants = cva( - 'fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500', + "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500", { variants: { side: { - top: 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top', + top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top", bottom: - 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom', - left: 'inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm', + "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom", + left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm", right: - 'inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm', + "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm", }, }, defaultVariants: { - side: 'right', + side: "right", }, } -); +) interface SheetContentProps extends React.ComponentPropsWithoutRef, @@ -54,7 +54,7 @@ interface SheetContentProps const SheetContent = React.forwardRef< React.ElementRef, SheetContentProps ->(({ side = 'right', className, children, ...props }, ref) => ( +>(({ side = "right", className, children, ...props }, ref) => ( {children} - - + + + Close -)); -SheetContent.displayName = SheetPrimitive.Content.displayName; +)) +SheetContent.displayName = SheetPrimitive.Content.displayName const SheetHeader = ({ className, @@ -77,13 +78,13 @@ const SheetHeader = ({ }: React.HTMLAttributes) => (
-); -SheetHeader.displayName = 'SheetHeader'; +) +SheetHeader.displayName = "SheetHeader" const SheetFooter = ({ className, @@ -91,13 +92,13 @@ const SheetFooter = ({ }: React.HTMLAttributes) => (
-); -SheetFooter.displayName = 'SheetFooter'; +) +SheetFooter.displayName = "SheetFooter" const SheetTitle = React.forwardRef< React.ElementRef, @@ -105,11 +106,11 @@ const SheetTitle = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetTitle.displayName = SheetPrimitive.Title.displayName; +)) +SheetTitle.displayName = SheetPrimitive.Title.displayName const SheetDescription = React.forwardRef< React.ElementRef, @@ -117,11 +118,11 @@ const SheetDescription = React.forwardRef< >(({ className, ...props }, ref) => ( -)); -SheetDescription.displayName = SheetPrimitive.Description.displayName; +)) +SheetDescription.displayName = SheetPrimitive.Description.displayName export { Sheet, @@ -134,4 +135,4 @@ export { SheetFooter, SheetTitle, SheetDescription, -}; +} diff --git a/src/contexts/DonationCartContext/DonationCartContext.tsx b/src/contexts/DonationCartContext/DonationCartContext.tsx new file mode 100644 index 00000000..138bf9f6 --- /dev/null +++ b/src/contexts/DonationCartContext/DonationCartContext.tsx @@ -0,0 +1,99 @@ +import React, { createContext, useCallback, useEffect, useState } from 'react'; + +import { IDonationCartContext, IDonationCartItem } from './types'; + +function getDonationCart(): Record { + const data = localStorage.getItem('shelter-carts'); + if (!data) return {}; + return JSON.parse(data); +} + +const DonationCartContext = createContext({} as IDonationCartContext); + +const DonationCartProvider = ({ children }: { children?: React.ReactNode }) => { + const [opened, setOpened] = useState(false); + const [carts, setCarts] = useState>( + getDonationCart() + ); + + const addItem = useCallback((shelterId: string, item: IDonationCartItem) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + if (prev.some((p) => p.id === item.id)) + return { + ...state, + [shelterId]: prev.map((p) => + p.id === item.id + ? { ...p, quantity: p.quantity + item.quantity } + : p + ), + }; + else return { ...state, [shelterId]: [...prev, item] }; + }); + }, []); + + const updateItem = useCallback( + ( + shelterId: string, + supplyId: string, + payload: Partial> + ) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + if (prev.some((p) => p.id === supplyId)) + return { + ...state, + [shelterId]: prev.map((p) => + p.id === supplyId ? { ...p, ...payload } : p + ), + }; + else return state; + }); + }, + [] + ); + + const removeItem = useCallback((shelterId: string, supplyId: string) => { + setCarts((state) => { + const prev = state[shelterId] ?? []; + return { ...state, [shelterId]: prev.filter((p) => p.id !== supplyId) }; + }); + }, []); + + const toggleOpened = useCallback(() => setOpened((prev) => !prev), []); + + const clearCart = useCallback( + (shelterId: string) => setCarts((state) => ({ ...state, [shelterId]: [] })), + [] + ); + + const updateCart = useCallback( + (shelterId: string, items: IDonationCartItem[]) => { + setCarts((prev) => ({ ...prev, [shelterId]: items })); + }, + [] + ); + + useEffect(() => { + localStorage.setItem('shelter-carts', JSON.stringify(carts)); + }, [carts]); + + return ( + + {children} + + ); +}; + +export { DonationCartContext, DonationCartProvider }; diff --git a/src/contexts/DonationCartContext/index.ts b/src/contexts/DonationCartContext/index.ts new file mode 100644 index 00000000..a66d1068 --- /dev/null +++ b/src/contexts/DonationCartContext/index.ts @@ -0,0 +1,6 @@ +import { + DonationCartContext, + DonationCartProvider, +} from './DonationCartContext'; + +export { DonationCartContext, DonationCartProvider }; diff --git a/src/contexts/DonationCartContext/types.ts b/src/contexts/DonationCartContext/types.ts new file mode 100644 index 00000000..14539cb7 --- /dev/null +++ b/src/contexts/DonationCartContext/types.ts @@ -0,0 +1,25 @@ +import { SupplyMeasure } from '@/hooks/useShelter/types'; +import { SupplyPriority } from '@/service/supply/types'; + +export interface IDonationCartItem { + id: string; + name: string; + quantity: number; + priority: SupplyPriority; + measure: SupplyMeasure; +} + +export interface IDonationCartContext { + carts: Record; + opened: boolean; + toggleOpened: () => void; + addItem: (shelterId: string, item: IDonationCartItem) => void; + removeItem: (shelterId: string, supplyId: string) => void; + updateItem: ( + shelterId: string, + supplyId: string, + payload: Partial> + ) => void; + clearCart: (shelterId: string) => void; + updateCart: (shelterId: string, items: IDonationCartItem[]) => void; +} diff --git a/src/contexts/index.ts b/src/contexts/index.ts index 2d3ea5bd..7eeef33b 100644 --- a/src/contexts/index.ts +++ b/src/contexts/index.ts @@ -1,3 +1,12 @@ -import { SessionContext, SessionProvider } from "./SessionContext"; +import { SessionContext, SessionProvider } from './SessionContext'; +import { + DonationCartContext, + DonationCartProvider, +} from './DonationCartContext'; -export { SessionContext, SessionProvider }; +export { + SessionContext, + SessionProvider, + DonationCartContext, + DonationCartProvider, +}; diff --git a/src/hooks/index.ts b/src/hooks/index.ts index a8007959..f28c05ed 100644 --- a/src/hooks/index.ts +++ b/src/hooks/index.ts @@ -12,6 +12,7 @@ import { usePartners } from './usePartners'; import { useGithubContributors } from './useGithubContributors'; import { useAuthRoles } from './useAuthRoles'; import { useSupporters } from './useSupporters'; +import { useDonationOrder } from './useDonationOrder'; export { useShelters, @@ -28,4 +29,5 @@ export { useGithubContributors, useAuthRoles, useSupporters, + useDonationOrder, }; diff --git a/src/hooks/useDonationOrder/index.ts b/src/hooks/useDonationOrder/index.ts new file mode 100644 index 00000000..f0211436 --- /dev/null +++ b/src/hooks/useDonationOrder/index.ts @@ -0,0 +1,3 @@ +import { useDonationOrder } from './useDonationOrder'; + +export { useDonationOrder }; diff --git a/src/hooks/useDonationOrder/types.ts b/src/hooks/useDonationOrder/types.ts new file mode 100644 index 00000000..e15144de --- /dev/null +++ b/src/hooks/useDonationOrder/types.ts @@ -0,0 +1,57 @@ +export enum ShelterCategory { + Shelter = 'Shelter', + DistributionCenter = 'DistributionCenter', +} + +export enum SupplyMeasure { + Unit = 'Unit', + Kg = 'Kg', + Litters = 'Litters', + Box = 'Box', + Piece = 'Piece', +} + +export interface IUseShelterData { + id: string; + name: string; + street?: string; + neighbourhood?: string; + city?: string; + streetNumber?: string | null; + zipCode?: string; + address: string; + pix?: string | null; + shelteredPeople?: number | null; + capacity?: number | null; + contact?: string | null; + petFriendly?: boolean | null; + prioritySum: number; + verified: boolean; + latitude?: string | null; + longitude?: string | null; + shelterSupplies: IUseShelterDataSupply[]; + category: ShelterCategory; + actived: boolean; + createdAt: string; + updatedAt?: string | null; +} + +export interface IUseShelterDataSupply { + priority: number; + quantity?: number | null; + supply: IUseShelterDataSupplyData; +} + +export interface IUseShelterDataSupplyData { + id: string; + name: string; + measure: SupplyMeasure; + supplyCategory: IUseShelterDataSupplyCategory; + createdAt: string; + updatedAt?: string | null; +} + +export interface IUseShelterDataSupplyCategory { + id: string; + name: string; +} diff --git a/src/hooks/useDonationOrder/useDonationOrder.tsx b/src/hooks/useDonationOrder/useDonationOrder.tsx new file mode 100644 index 00000000..f80513fe --- /dev/null +++ b/src/hooks/useDonationOrder/useDonationOrder.tsx @@ -0,0 +1,8 @@ +import { IDonateOrderItem } from '@/service/donationOrder/types'; +import { useFetch } from '../useFetch'; + +const useDonationOrder = (donationOrderId: string) => { + return useFetch(`/donation/order/${donationOrderId}`); +}; + +export { useDonationOrder }; diff --git a/src/hooks/useFetch/useFetch.tsx b/src/hooks/useFetch/useFetch.tsx index 121f5795..887833bd 100644 --- a/src/hooks/useFetch/useFetch.tsx +++ b/src/hooks/useFetch/useFetch.tsx @@ -13,7 +13,8 @@ function useFetch(path?: string, options: IUseFetchOptions = {}) { const refresh = useCallback( (config?: AxiosRequestConfig) => { const headers = config?.headers ?? {}; - if (cache) headers['x-app-cache'] = 'true'; + if (cache && import.meta.env.VITE_REQUEST_CACHE !== 'false') + headers['x-app-cache'] = 'true'; setLoading(true); if (path) { diff --git a/src/hooks/usePaginatedQuery/paths.ts b/src/hooks/usePaginatedQuery/paths.ts index 5c1d2cc5..6dde3171 100644 --- a/src/hooks/usePaginatedQuery/paths.ts +++ b/src/hooks/usePaginatedQuery/paths.ts @@ -3,4 +3,5 @@ export enum PaginatedQueryPath { ShelterCities = '/shelters/cities', SupplyCategories = '/supply-categories', Supplies = '/supplies', + DonationOrder = '/donation/order', } diff --git a/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx b/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx index 11009abd..4229807e 100644 --- a/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx +++ b/src/hooks/usePaginatedQuery/usePaginatedQuery.tsx @@ -6,7 +6,10 @@ import { PaginatedQueryPath } from './paths'; import { IPaginatedResponse } from './types'; import { IServerResponse } from '@/types'; -function usePaginatedQuery(path: string | PaginatedQueryPath) { +function usePaginatedQuery( + path: string | PaginatedQueryPath, + defaultConfig: AxiosRequestConfig = {} +) { const [loading, setLoading] = useState(false); const [data, setData] = useState>({ count: 0, @@ -16,14 +19,17 @@ function usePaginatedQuery(path: string | PaginatedQueryPath) { }); const refresh = useCallback( - (config?: AxiosRequestConfig) => { + (config: AxiosRequestConfig = {}) => { setLoading(true); api - .get>>(path, config) + .get>>(path, { + ...defaultConfig, + ...config, + }) .then(({ data }) => setData(data.data)) .finally(() => setLoading(false)); }, - [path] + [defaultConfig, path] ); useEffect(() => { diff --git a/src/hooks/useShelter/types.ts b/src/hooks/useShelter/types.ts index 22c3bd67..e15144de 100644 --- a/src/hooks/useShelter/types.ts +++ b/src/hooks/useShelter/types.ts @@ -3,6 +3,14 @@ export enum ShelterCategory { DistributionCenter = 'DistributionCenter', } +export enum SupplyMeasure { + Unit = 'Unit', + Kg = 'Kg', + Litters = 'Litters', + Box = 'Box', + Piece = 'Piece', +} + export interface IUseShelterData { id: string; name: string; @@ -37,6 +45,7 @@ export interface IUseShelterDataSupply { export interface IUseShelterDataSupplyData { id: string; name: string; + measure: SupplyMeasure; supplyCategory: IUseShelterDataSupplyCategory; createdAt: string; updatedAt?: string | null; diff --git a/src/hooks/useShelters/types.ts b/src/hooks/useShelters/types.ts index 057bfcbe..4ad161e6 100644 --- a/src/hooks/useShelters/types.ts +++ b/src/hooks/useShelters/types.ts @@ -1,5 +1,5 @@ import { ShelterTagType } from '@/pages/Home/components/ShelterListItem/types'; -import { ShelterCategory } from '../useShelter/types'; +import { ShelterCategory, SupplyMeasure } from '../useShelter/types'; export interface IUseSheltersData { id: string; @@ -29,6 +29,7 @@ export interface IUseSheltersData { export interface IUseSheltersDataSupplyData { supply: { name: string; + measure: SupplyMeasure; supplyCategory: { name: string }; }; priority: number; diff --git a/src/hooks/useShelters/useShelters.tsx b/src/hooks/useShelters/useShelters.tsx index f6959800..13e51ac4 100644 --- a/src/hooks/useShelters/useShelters.tsx +++ b/src/hooks/useShelters/useShelters.tsx @@ -20,7 +20,8 @@ const useShelters = (options: IUseShelterOptions = {}) => { (config: AxiosRequestConfig = {}, append: boolean = false) => { const { search, ...rest } = (config ?? {}).params ?? {}; const headers = config.headers ?? {}; - if (cache) headers['x-app-cache'] = 'true'; + if (cache && import.meta.env.VITE_REQUEST_CACHE !== 'false') + headers['x-app-cache'] = 'true'; if (!append) setLoading(true); api .get>('/shelters', { diff --git a/src/lib/utils.ts b/src/lib/utils.ts index c12c1b59..42513669 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -1,4 +1,4 @@ -import { ShelterCategory } from '@/hooks/useShelter/types'; +import { ShelterCategory, SupplyMeasure } from '@/hooks/useShelter/types'; import { IUseSheltersDataSupplyData } from '@/hooks/useShelters/types'; import { ShelterTagInfo, @@ -150,6 +150,14 @@ function checkIsNull(v?: any | null) { return v !== null && v !== undefined; } +const SupplyMeasureMap: Record = { + Box: 'caixa(s)', + Kg: 'kg', + Litters: 'litro(s)', + Piece: 'peça(s)', + Unit: 'un', +}; + export { cn, getAvailabilityProps, @@ -160,4 +168,5 @@ export { removeDuplicatesByField, normalizedCompare, checkIsNull, + SupplyMeasureMap, }; diff --git a/src/main.tsx b/src/main.tsx index 471f7fc0..cfc80cbd 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,5 +1,10 @@ -import ReactDOM from "react-dom/client"; -import { App } from "./App.tsx"; -import "./global.css"; +import ReactDOM from 'react-dom/client'; +import { App } from './App.tsx'; +import './global.css'; -ReactDOM.createRoot(document.getElementById("root")!).render(); +document.documentElement.style.setProperty( + '--vh', + `${window.innerHeight * 0.01}px` +); + +ReactDOM.createRoot(document.getElementById('root')!).render(); diff --git a/src/pages/CreateSupply/CreateSupply.tsx b/src/pages/CreateSupply/CreateSupply.tsx index 18fd1a3a..78f8025c 100644 --- a/src/pages/CreateSupply/CreateSupply.tsx +++ b/src/pages/CreateSupply/CreateSupply.tsx @@ -6,7 +6,7 @@ import * as Yup from 'yup'; import { CircleStatus, Header, LoadingScreen, TextField } from '@/components'; import { Button } from '@/components/ui/button'; import { useToast } from '@/components/ui/use-toast'; -import { useSupplyCategories } from '@/hooks'; +import { useShelter, useSupplies, useSupplyCategories } from '@/hooks'; import { Select, SelectContent, @@ -19,12 +19,21 @@ import { getSupplyPriorityProps } from '@/lib/utils'; import { ShelterSupplyServices, SupplyServices } from '@/service'; import { ICreateShelterSupply } from '@/service/shelterSupply/types'; import { clearCache } from '@/api/cache'; +import { Fragment } from 'react/jsx-runtime'; +import { IUseSuppliesData } from '@/hooks/useSupplies/types'; +import { useState } from 'react'; +import { ModalCreateSupply } from './components'; const CreateSupply = () => { const navigate = useNavigate(); const { shelterId = '-1' } = useParams(); + const { data: shelter } = useShelter(shelterId); const { toast } = useToast(); const { data: supplyCategories, loading } = useSupplyCategories(); + const { data: supplies } = useSupplies(); + + const [modalOpened, setModalOpened] = useState(false); + const [supplyId, setSupplyId] = useState(''); const { errors, @@ -36,9 +45,8 @@ const CreateSupply = () => { } = useFormik>({ initialValues: { name: '', - supplyCategoryId: supplyCategories?.at(0)?.id ?? '-1', shelterId, - priority: SupplyPriority.NotNeeded, + priority: SupplyPriority.Needing, }, enableReinitialize: true, validateOnBlur: false, @@ -46,8 +54,18 @@ const CreateSupply = () => { validateOnMount: false, validationSchema: Yup.object().shape({ shelterId: Yup.string().required('Este campo deve ser preenchido'), - name: Yup.string().required('Este campo deve ser preenchido'), - quantity: Yup.number().typeError('Insira um valor númerico').moreThan(0, 'O valor tem que ser maior do que 0').optional(), + name: Yup.string() + .matches(/^[a-zA-ZÀ-ÿ0-9\s]*$/, "O nome não deve conter caracteres especiais") + .test('min-letters', 'O nome deve conter pelo menos 3 letras', value => { + const letterCount = (value?.match(/[a-zA-ZÀ-ÿ]/g) || []).length; + return letterCount >= 3; + }) + .min(3, 'Insira no mínimo 3 caracteres') + .required('Este campo deve ser preenchido'), + quantity: Yup.number() + .typeError('Insira um valor númerico') + .moreThan(0, 'O valor tem que ser maior do que 0') + .optional(), priority: Yup.string().required('Este campo deve ser preenchido'), supplyCategoryId: Yup.string().required('Este campo deve ser preenchido'), }), @@ -78,122 +96,201 @@ const CreateSupply = () => { }, }); + const filteredSupplies = + values.name.length > 2 + ? supplies.filter((e) => { + const normalizedSearchTerm = values.name + .trim() + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, ''); + return e.name + .toLowerCase() + .normalize('NFD') + .replace(/[\u0300-\u036f]/g, '') + .includes(normalizedSearchTerm); + }) + : []; + + const renderSupplies = (element: IUseSuppliesData, key: number) => { + if (values.name.length < 3 || key > 30) return <>; + + return ( + +
{ + setSupplyId(element.id); + setModalOpened(true); + }} + > + {element.name} +
+
+ ); + }; + if (loading) return ; return ( -
-
navigate(-1)} - > - - - } - /> -
-
-
Cadastrar novo item
-

- Informe o nome do item que você deseja cadastrar, a categoria e a - prioridade -

-
- -
- - setFieldValue('supplyCategoryId', v)} + > + + - {category.name} - - ))} - - -
- -
- - + ))} + + +
+ {errors.supplyCategoryId} +
+
+ +
+ + +
+
{errors.priority}
-
-
- -
- +
+ +
+ +
-
+ ); }; diff --git a/src/pages/CreateSupply/components/index.ts b/src/pages/CreateSupply/components/index.ts new file mode 100644 index 00000000..87db6c65 --- /dev/null +++ b/src/pages/CreateSupply/components/index.ts @@ -0,0 +1,3 @@ +import { ModalCreateSupply } from './modal'; + +export { ModalCreateSupply }; diff --git a/src/pages/CreateSupply/components/modal.tsx b/src/pages/CreateSupply/components/modal.tsx new file mode 100644 index 00000000..ce4a838e --- /dev/null +++ b/src/pages/CreateSupply/components/modal.tsx @@ -0,0 +1,191 @@ +import { useState, useEffect } from 'react'; + +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'; +import { Label } from '@/components/ui/label'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { ShelterSupplyServices } from '@/service'; +import { Link } from 'react-router-dom'; +import { IMessage, IProps, ISupplies } from './types'; + +const ModalCreateSupply = (props: IProps) => { + const { + open, + onClose, + title, + description = '', + options, + supplies, + supplyId, + shelterId, + } = props; + const [data, setData] = useState({ + quantity: 0, + priority: '10', + }); + const [message, setMessage] = useState({ + error: false, + register: false, + successSub: false, + message: '', + }); + + useEffect(() => { + supplies.forEach((element: ISupplies) => { + if (element.supply.id === supplyId) { + setMessage((prev) => ({ + ...prev, + register: true, + message: 'Verificamos que você já tem esse item registrado.', + })); + setData({ + quantity: element.quantity, + priority: element.priority.toString(), + }); + } + }); + }, [supplies, supplyId]); + + const onSave = () => { + if (!message.register) { + ShelterSupplyServices.create({ + shelterId, + supplyId, + priority: parseInt(data.priority), + quantity: data.quantity, + }) + .then(() => { + setMessage((prev) => ({ + ...prev, + successSub: true, + message: 'Registro salvo com sucesso!', + })); + }) + .catch(() => + alert( + 'Ocorreu um erro. Por favor, tente novamente ou entre em contato com o suporte.' + ) + ); + } else { + ShelterSupplyServices.update(shelterId, supplyId, { + priority: parseInt(data.priority), + quantity: data.quantity, + }) + .then(() => { + setMessage((prev) => ({ + ...prev, + successSub: true, + message: 'Registro salvo com sucesso!', + })); + }) + .catch(() => + alert( + 'Ocorreu um erro. Por favor, tente novamente ou entre em contato com o suporte.' + ) + ); + } + }; + + return ( + + + {message.successSub ? ( + <> +
+ {message.message} +
+ + + + + + + ) : ( + <> + + + {title} + + {description && ( + {description} + )} + + {message.message && ( +
+ {message.message} +
+ )} +
+ +
+ + setData((prev) => ({ + ...prev, + quantity: parseInt(event.target.value), + })) + } + placeholder="Quantidade" + min={0} + /> +
+
+
+ + setData((prev: any) => ({ + ...prev, + priority: v, + })) + } + > + {options.map((option: any, idx: any) => ( +
+ + +
+ ))} +
+
+ + + + + )} +
+
+ ); +}; + +export { ModalCreateSupply }; diff --git a/src/pages/CreateSupply/components/types.ts b/src/pages/CreateSupply/components/types.ts new file mode 100644 index 00000000..27af8ed0 --- /dev/null +++ b/src/pages/CreateSupply/components/types.ts @@ -0,0 +1,35 @@ +import { IDialogSelectorItemProps } from '@/components/DialogSelector/types'; + +export interface IProps { + open: boolean; + onClose?: () => void; + title: string; + description?: string; + options: IDialogSelectorItemProps[]; + supplies: [ + { + supply: { + id: string; + }; + quantity: number; + priority: string; + } + ]; + supplyId: string; + shelterId: string; +} + +export interface ISupplies { + supply: { + id: string; + }; + quantity: number; + priority: string; +} + +export interface IMessage { + error: boolean; + message: string; + register: boolean; + successSub: boolean; +} diff --git a/src/pages/EditShelterSupply/components/SupplyRow/SupplyRow.tsx b/src/pages/EditShelterSupply/components/SupplyRow/SupplyRow.tsx index a4d59c7d..9aba6cd4 100644 --- a/src/pages/EditShelterSupply/components/SupplyRow/SupplyRow.tsx +++ b/src/pages/EditShelterSupply/components/SupplyRow/SupplyRow.tsx @@ -1,13 +1,19 @@ import { SupplyPriority } from '@/service/supply/types'; import { SupplyRowInfo } from '../SupplyRowInfo'; import { ISupplyRowProps } from './types'; +import { Archive } from 'lucide-react'; const SupplyRow = (props: ISupplyRowProps) => { const { name, items, onClick } = props; return ( -
-

{name}

+
+
+

+ + {name.toUpperCase()} +

+
{items.map((item, idy) => ( { + const storedFilterData = JSON.parse(localStorage.getItem('filter-data') || '{}'); + return { ...initialFilterData, ...storedFilterData, ...qs.parse(new URLSearchParams(window.location.search).toString()) }; +}; + +const saveFilterData = (filterData: IFilterFormProps) => { + localStorage.setItem('filter-data', JSON.stringify(filterData)); +}; + const Home = () => { const { data: shelters, loading, refresh } = useShelters({ cache: true }); const [isModalOpen, setOpenModal] = useState(false); const [, setSearchParams] = useSearchParams(); - const [filterData, setFilterData] = useState({ - ...initialFilterData, - ...qs.parse(new URLSearchParams(window.location.search).toString()), - }); + const [filterData, setFilterData] = useState(loadFilterData()); const [, setSearch] = useThrottle( { throttle: 400, callback: () => { const params = new URLSearchParams(qs.stringify(filterData)); - setSearchParams(params); - refresh({ - params: params, - }); + refresh({ params }); }, }, - [] + [filterData] ); const clearSearch = useCallback(() => { setSearch(''); setFilterData(initialFilterData); + localStorage.removeItem('filter-data'); setSearchParams(''); refresh(); }, [refresh, setSearch, setSearchParams]); @@ -54,19 +58,26 @@ const Home = () => { [shelters.page, shelters.perPage, shelters.count] ); + const factorySearchArgs = useCallback((values: IFilterFormProps) => { + const searchQueryArgs = { + search: values.search, + priorities: values.priorities, + supplyCategoryIds: values.supplyCategoryIds, + supplyIds: values.supplyIds, + shelterStatus: values.shelterStatus, + cities: values.cities, + }; + return searchQueryArgs; + }, []); + const onSubmitFilterForm = useCallback( (values: IFilterFormProps) => { setOpenModal(false); setFilterData(values); - const searchQuery = qs.stringify(values, { - skipNulls: true, - }); + const searchQuery = qs.stringify(values, { skipNulls: true }); setSearchParams(searchQuery); - refresh({ - params: { - search: searchQuery, - }, - }); + saveFilterData(values); + refresh({ params: { search: searchQuery } }); }, [refresh, setSearchParams] ); @@ -76,16 +87,18 @@ const Home = () => { ...shelters.filters, page: shelters.page + 1, perPage: shelters.perPage, - search: qs.stringify(filterData), + search: qs.stringify(factorySearchArgs(filterData)), }; + refresh({ params }, true); + }, [refresh, filterData, shelters.filters, shelters.page, shelters.perPage, factorySearchArgs]); - refresh( - { - params: params, - }, - true - ); - }, [refresh, filterData, shelters.filters, shelters.page, shelters.perPage]); + useEffect(() => { + if (filterData.search || filterData.cities.length > 0 || filterData.priorities.length > 0 || filterData.shelterStatus.length > 0 || filterData.supplyCategoryIds.length > 0 || filterData.supplyIds.length > 0){ + setSearchParams(qs.stringify(filterData)); + refresh({ params: { search: qs.stringify(filterData) } }); + } + saveFilterData(filterData); + }, [filterData, refresh, setSearchParams]); return (
diff --git a/src/pages/Home/components/Filter/Filter.tsx b/src/pages/Home/components/Filter/Filter.tsx index 59caf9eb..d19ebf1b 100644 --- a/src/pages/Home/components/Filter/Filter.tsx +++ b/src/pages/Home/components/Filter/Filter.tsx @@ -19,7 +19,6 @@ import { import { IFilterFormikProps, IFilterProps, - ISelectField, ShelterAvailabilityStatus, } from './types'; import { priorityOptions } from '@/lib/utils'; @@ -66,12 +65,10 @@ const Filter = (props: IFilterProps) => { { initialValues: { cities: data.cities ?? [], - priority: data.priority - ? { - label: priorityOpts[data.priority], - value: data.priority, - } - : null, + priorities: data.priorities.map((p: string) => ({ + label: priorityOpts[Number(p) as SupplyPriority], + value: p, + })), search: data.search, shelterStatus: data.shelterStatus.map((s) => ({ label: ShelterAvailabilityStatusMapped[s], @@ -95,7 +92,7 @@ const Filter = (props: IFilterProps) => { }), onSubmit: (values) => { const { - priority, + priorities, search, shelterStatus, supplies, @@ -103,7 +100,7 @@ const Filter = (props: IFilterProps) => { cities, } = values; onSubmit({ - priority: priority?.value ? +priority.value : null, + priorities: priorities.map((p) => p.value), search, shelterStatus: shelterStatus.map((s) => s.value), supplyCategoryIds: supplyCategories.map((s) => s.value), @@ -160,9 +157,7 @@ const Filter = (props: IFilterProps) => {
- setFieldValue('search', ev.target.value ?? '') - } + onChange={(v) => setFieldValue('search', v)} />
@@ -186,21 +181,15 @@ const Filter = (props: IFilterProps) => {