diff --git a/package-lock.json b/package-lock.json index bf89fcf023a227e3f33c5e07c183e07c02835242..70fc0b892fc35674af8fa6e2351cfe094cd20640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "@types/node": "^20.8.7", "axios": "^1.6.1", "clsx": "^2.0.0", + "fuse.js": "^7.0.0", "localforage": "^1.10.0", "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-paginate": "^8.2.0", "react-query": "^3.39.3", "react-router-dom": "^6.17.0", "sort-by": "^1.2.0", @@ -2322,6 +2324,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fuse.js": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/fuse.js/-/fuse.js-7.0.0.tgz", + "integrity": "sha512-14F4hBIxqKvD4Zz/XjDc3y94mNZN6pRv3U13Udo0lNLCWRBUsrMv2xwcF/y/Z5sV6+FQW+/ow68cHpm4sunt8Q==", + "engines": { + "node": ">=10" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -2850,7 +2860,6 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "dev": true, "engines": { "node": ">=0.10.0" } @@ -3161,6 +3170,16 @@ "node": ">= 0.8.0" } }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-from-env": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", @@ -3218,6 +3237,22 @@ "react": "^18.2.0" } }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-paginate": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/react-paginate/-/react-paginate-8.2.0.tgz", + "integrity": "sha512-sJCz1PW+9PNIjUSn919nlcRVuleN2YPoFBOvL+6TPgrH/3lwphqiSOgdrLafLdyLDxsgK+oSgviqacF4hxsDIw==", + "dependencies": { + "prop-types": "^15" + }, + "peerDependencies": { + "react": "^16 || ^17 || ^18" + } + }, "node_modules/react-query": { "version": "3.39.3", "resolved": "https://registry.npmjs.org/react-query/-/react-query-3.39.3.tgz", diff --git a/package.json b/package.json index 9885a281879256810cbd55b2b6baa7d18c84ccb6..2def9b4a7cf023ab476c8b0606e36187b4d0e855 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,12 @@ "@types/node": "^20.8.7", "axios": "^1.6.1", "clsx": "^2.0.0", + "fuse.js": "^7.0.0", "localforage": "^1.10.0", "match-sorter": "^6.3.1", "react": "^18.2.0", "react-dom": "^18.2.0", + "react-paginate": "^8.2.0", "react-query": "^3.39.3", "react-router-dom": "^6.17.0", "sort-by": "^1.2.0", diff --git a/src/components/gym/GymCard.tsx b/src/components/gym/GymCard.tsx index f052113f6831764c3a0255e09ff60cf57c8d6648..3c87c6b9c859c4c08722a0b813ff81a186b256ba 100644 --- a/src/components/gym/GymCard.tsx +++ b/src/components/gym/GymCard.tsx @@ -1,6 +1,5 @@ import config from "@/utils/config"; import { GymReturned } from "@/utils/validationSchema/gym"; -import React from "react"; import { Link } from "react-router-dom"; function GymCard({ diff --git a/src/pages/gym/Gym.tsx b/src/pages/gym/Gym.tsx index 6c6322588ea55ba62ada5f46ad6e469fbbe47595..267f9bb9eb572429da7d50e9fb47ac3cd3208575 100644 --- a/src/pages/gym/Gym.tsx +++ b/src/pages/gym/Gym.tsx @@ -1,19 +1,47 @@ import GymCard from "@/components/gym/GymCard"; import { useAllGyms } from "@/utils/context/GymProvider"; +import useDebounce from "@/utils/hooks/useDebounce"; +import useField from "@/utils/hooks/useField"; +import validate from "@/utils/validationSchema/validate"; import { useEffect } from "react"; +import { z } from "zod"; function Gym() { - const { allGyms, allGymsStatus } = useAllGyms(); + const { allGyms, allGymsStatus, setSearch: executeGymSearch } = useAllGyms(); useEffect(() => { console.log(allGyms); console.log(allGymsStatus); }, [allGyms, allGymsStatus]); + const { field: search, setField: setSearch } = useField( + (search) => validate(z.string(), search), + "" + ); + useDebounce( + () => { + executeGymSearch(search); + }, + 1000, + [search] + ); if (allGymsStatus !== "success" || !allGyms) { return <div>Loading...</div>; } return ( - <div className="w-full flex flex-col items-center justify-start py-4 lg:py-6"> + <div className="w-full flex flex-col items-center justify-start py-4 lg:py-6 gap-8"> + <div className="flex flex-col items-center"> + <label className="label text-center" htmlFor="username"> + Search for a Gym + </label> + <input + id="username" + className="input input-primary" + value={search} + onChange={(e) => { + setSearch(e.target.value); + }} + /> + </div> <div className="grid gap-4 lg:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 max-w-7xl"> {allGyms.map((gym) => ( <GymCard gym={gym} key={gym.id} /> diff --git a/src/pages/gym/GymIndividual.tsx b/src/pages/gym/GymIndividual.tsx index 41998b45925b5775410fb7aba27a7c4931e12935..6bb18accedc98d249cb00b14b7bfc4bb25a05d4c 100644 --- a/src/pages/gym/GymIndividual.tsx +++ b/src/pages/gym/GymIndividual.tsx @@ -1,8 +1,17 @@ +import { useGym } from "@/utils/context/GymProvider"; import { useParams } from "react-router-dom"; function GymIndividual() { const { id } = useParams(); - return <div>{id}</div>; + const { data, status } = useGym(Number(id ?? 1)); + if (!data || status !== "success") { + return "Loading..."; + } + return ( + <div className="w-full flex flex-col max-w-7xl mx-auto"> + <p>{data.name}</p> + </div> + ); } export default GymIndividual; diff --git a/src/utils/api/gym.ts b/src/utils/api/gym.ts index 74860d6aaa0ac487d31db5b0cf2a2159fd69dcfa..308ab405aab2f752e3784c1a28df53bcf663958a 100644 --- a/src/utils/api/gym.ts +++ b/src/utils/api/gym.ts @@ -1,6 +1,6 @@ -import axios from "axios"; -import config from "../config"; -import { header } from "."; +// import axios from "axios"; +// import config from "../config"; +// import { header } from "."; import { GymReturned } from "../validationSchema/gym"; export async function getGym(): Promise<GymReturned[]> { @@ -35,10 +35,21 @@ export async function getGym(): Promise<GymReturned[]> { return myPromise; } -export async function getGymById({ - id, -}: { - id: number; -}): Promise<GymReturned[]> { - return (await axios.get(`${config.NODE_JS_API}/api/gym/${id}/`, header)).data; +export async function getGymById({ id }: { id: number }): Promise<GymReturned> { + // return (await axios.get(`${config.NODE_JS_API}/api/gym/${id}/`, header)).data; + const myPromise = new Promise<GymReturned>(function (myResolve) { + myResolve({ + averageRating: 5, + cityId: 1, + cityName: "Bruh City", + description: "dfklcdmfklm", + id: id, + monthlyPrice: 5000, + name: "Bruh", + pictureId: 1, + videoId: 1, + }); + }); + + return myPromise; } diff --git a/src/utils/context/GymProvider.tsx b/src/utils/context/GymProvider.tsx index 0750c3ba2eed6d57c1d6780ef27921bf0d68f16c..f9b877b67ae0022f26e1cd48782cba650109b398 100644 --- a/src/utils/context/GymProvider.tsx +++ b/src/utils/context/GymProvider.tsx @@ -1,27 +1,30 @@ -import { ReactNode, createContext, useContext } from "react"; +import { ReactNode, createContext, useContext, useState } from "react"; import { QueryStatus, useQuery } from "react-query"; import { getGym, getGymById } from "../api/gym"; import { GymReturned } from "../validationSchema/gym"; +import Fuse from "fuse.js"; const AllGymsContext = createContext<{ allGyms: GymReturned[] | undefined; allGymsStatus: QueryStatus; -}>({ allGyms: undefined, allGymsStatus: "idle" }); + setSearch: (arg0: string) => void; + search: string | undefined; +}>({ + allGyms: undefined, + allGymsStatus: "idle", + setSearch: (arg0: string) => {}, + search: undefined, +}); export function useAllGyms() { return useContext(AllGymsContext); } export function useGym(id: number) { - return useQuery([ - "gyms", - String(id), - async () => await getGymById({ id }), - { - cacheTime: 300000, - retry: 3, - }, - ]); + return useQuery(["gyms", String(id)], async () => await getGymById({ id }), { + cacheTime: 300000, + retry: 3, + }); } function GymProvider({ children }: { children: ReactNode }) { @@ -29,9 +32,23 @@ function GymProvider({ children }: { children: ReactNode }) { cacheTime: 300000, retry: 3, }); + const [search, setSearch] = useState<undefined | string>(undefined); return ( - <AllGymsContext.Provider value={{ allGyms, allGymsStatus }}> + <AllGymsContext.Provider + value={{ + allGyms: search + ? new Fuse(allGyms ?? [], { + keys: ["name"], + }) + .search(search) + .map((result) => result.item) + : allGyms, + allGymsStatus, + search, + setSearch, + }} + > {children} </AllGymsContext.Provider> ); diff --git a/src/utils/hooks/useDebounce.ts b/src/utils/hooks/useDebounce.ts new file mode 100644 index 0000000000000000000000000000000000000000..bd31aab4ce5c314de9a019e8e25c0484bee856e6 --- /dev/null +++ b/src/utils/hooks/useDebounce.ts @@ -0,0 +1,12 @@ +import { useEffect } from "react"; +import useTimeout from "./useTimeout"; + +export default function useDebounce( + callback: () => void, + delay: number, + dependencies: any[] +) { + const { reset, clear } = useTimeout(callback, delay); + useEffect(reset, [...dependencies, reset]); + useEffect(clear, [clear]); +} diff --git a/src/utils/hooks/useTimeout.ts b/src/utils/hooks/useTimeout.ts new file mode 100644 index 0000000000000000000000000000000000000000..e92d93a427a5240513c5cb6452c4466b5b6fdf06 --- /dev/null +++ b/src/utils/hooks/useTimeout.ts @@ -0,0 +1,30 @@ +import { useCallback, useEffect, useRef } from "react"; + +export default function useTimeout(callback: () => void, delay: number) { + const callbackRef = useRef(callback); + const timeoutRef = useRef<NodeJS.Timeout>(); + + useEffect(() => { + callbackRef.current = callback; + }, [callback]); + + const set = useCallback(() => { + timeoutRef.current = setTimeout(() => callbackRef.current(), delay); + }, [delay]); + + const clear = useCallback(() => { + timeoutRef.current && clearTimeout(timeoutRef.current); + }, []); + + useEffect(() => { + set(); + return clear; + }, [delay, set, clear]); + + const reset = useCallback(() => { + clear(); + set(); + }, [clear, set]); + + return { reset, clear }; +}