diff --git a/src/components/ProtectedLayout.tsx b/src/components/ProtectedLayout.tsx index 7e5f4c7849f54d175d6b6740d745cf2cd176fa1a..1f7aa9e60fe5bc6be2f2dc6c9699ac3b198c557b 100644 --- a/src/components/ProtectedLayout.tsx +++ b/src/components/ProtectedLayout.tsx @@ -1,5 +1,6 @@ import NavWrapper from "@/components/NavWrapper"; import { useUser } from "@/utils/context/AuthProvider"; +import GymApplicationProvider from "@/utils/context/GymApplicationProvider"; import GymProvider from "@/utils/context/GymProvider"; import { useEffect, useState } from "react"; import { useNavigate, Outlet } from "react-router-dom"; @@ -29,7 +30,9 @@ function ProtectedLayout() { return ( <NavWrapper> <GymProvider> - <Outlet /> + <GymApplicationProvider> + <Outlet /> + </GymApplicationProvider> </GymProvider> </NavWrapper> ); diff --git a/src/components/gym/GymApplication.tsx b/src/components/gym/GymApplication.tsx new file mode 100644 index 0000000000000000000000000000000000000000..6bc9080864ac36617c99462b20736de1eb7f135a --- /dev/null +++ b/src/components/gym/GymApplication.tsx @@ -0,0 +1,145 @@ +import { useGymApplications } from "@/utils/context/GymApplicationProvider"; +import useField from "@/utils/hooks/useField"; +import { GymReturned } from "@/utils/validationSchema/gym"; +import validate from "@/utils/validationSchema/validate"; +import { useState } from "react"; +import { z } from "zod"; +import ErrorLabel from "../ErrorLabel"; +import { useMutation, useQueryClient } from "react-query"; +import { + GymApplicationReturned, + GymApplicationSent, +} from "@/utils/validationSchema/gymApplication"; +import { useUser } from "@/utils/context/AuthProvider"; +import { applyToGym } from "@/utils/api/gymApplication"; + +function GymApplication({ gym }: { gym: GymReturned }) { + const { gymApplications, gymApplicationsStatus } = useGymApplications(); + const { + field: description, + fieldError: descriptionError, + setField: setDescription, + validate: validateDescription, + } = useField((description) => validate(z.string().min(1), description), ""); + const [disabled, setDisabled] = useState(false); + const [applyError, setApplyError] = useState(""); + const queryClient = useQueryClient(); + const { status, user } = useUser(); + + const { mutateAsync: applyMutation } = useMutation({ + mutationFn: async (payload: GymApplicationSent) => { + if (status !== "success" || user === undefined) { + return; + } + return await applyToGym(payload); + }, + onMutate: async (addedApplication: GymApplicationSent) => { + await queryClient.cancelQueries({ queryKey: ["applications"] }); + + const previousGymApplications = gymApplications; + + queryClient.setQueryData<GymApplicationReturned[] | undefined>( + ["applications"], + (oldGymApplications) => { + console.log("Previous gym applications", oldGymApplications); + if (!oldGymApplications) { + return oldGymApplications; + } + return [ + ...oldGymApplications, + { + acceptance: 0, + description: addedApplication.application_description, + gymId: addedApplication.gym_id, + gymName: addedApplication.gym_name, + }, + ]; + } + ); + return { previousGymApplications }; + }, + onError(_, __, context) { + queryClient.setQueryData( + ["applications"], + context?.previousGymApplications + ); + }, + onSuccess() { + console.log("Application", queryClient.getQueryData(["applications"])); + + // queryClient.invalidateQueries(["applications"]); + }, + }); + + async function handleSubmit() { + setDisabled(true); + if (descriptionError || user === undefined) { + return; + } + + try { + await applyMutation({ + application_description: description, + gym_id: gym.id, + gym_name: gym.name, + trainer_description: user.description, + trainer_name: user.name, + username: user.username, + }); + setDescription(""); + } catch (err: unknown) { + const error = err as { response: { data: { error: string } } }; + setApplyError(error.response.data.error); + } + setDisabled(false); + } + + if (gymApplicationsStatus !== "success") { + return <p>Loading...</p>; + } + + if (!gymApplications) { + return <p>Something wrong happened</p>; + } + + if (gymApplications.find(({ gymId }) => gymId === gym.id)) { + return ( + <button className="btn btn-disabled" disabled> + Applied + </button> + ); + } + + return ( + <> + <form + className="form-control gap-4 w-full" + onSubmit={(e) => { + e.preventDefault(); + handleSubmit(); + }} + > + <div className="flex flex-col"> + <label className="label" htmlFor="description"> + Description + </label> + <textarea + disabled={disabled} + id="description" + className="textarea textarea-bordered textarea-primary" + value={description} + onChange={(e) => { + setDescription(e.target.value); + validateDescription(e.target.value); + }} + /> + <ErrorLabel error={descriptionError} /> + </div> + <button className="btn btn-primary">Apply</button> + </form> + <ErrorLabel error={applyError} /> + </> + ); +} + +export default GymApplication; diff --git a/src/pages/gym/GymIndividual.tsx b/src/pages/gym/GymIndividual.tsx index 6bb18accedc98d249cb00b14b7bfc4bb25a05d4c..79e5b192e1a6f761af1433540a4d8f2bfa3033db 100644 --- a/src/pages/gym/GymIndividual.tsx +++ b/src/pages/gym/GymIndividual.tsx @@ -1,15 +1,34 @@ +import GymApplication from "@/components/gym/GymApplication"; +import config from "@/utils/config"; import { useGym } from "@/utils/context/GymProvider"; import { useParams } from "react-router-dom"; function GymIndividual() { const { id } = useParams(); - const { data, status } = useGym(Number(id ?? 1)); - if (!data || status !== "success") { + const { data: gym, status } = useGym(Number(id ?? 1)); + + if (status !== "success") { return "Loading..."; } + + if (!gym) { + return "Gym does not exist"; + } + return ( - <div className="w-full flex flex-col max-w-7xl mx-auto"> - <p>{data.name}</p> + <div className="w-full flex flex-col max-w-5xl mx-auto py-6 gap-4"> + <img + src={`${config.GYM_MEDIA_URL}/${gym.pictureId}.jpg`} + className="w-full" + /> + <div className="flex flex-col items-start justify-start text-left"> + <h1>{gym.name}</h1> + <p>{gym.monthlyPrice} / month</p> + <p>{gym.averageRating} stars</p> + <p>{gym.cityName}</p> + <p>{gym.description}</p> + </div> + <GymApplication gym={gym} /> </div> ); } diff --git a/src/utils/api/gymApplication.ts b/src/utils/api/gymApplication.ts index ec6058973a93fce291a8bf9c8a8554fb192acd7b..d5afd3ec2825dcc3d76eb5ace244190ad86dbc4c 100644 --- a/src/utils/api/gymApplication.ts +++ b/src/utils/api/gymApplication.ts @@ -1,6 +1,7 @@ import { GymApplicationFetched, GymApplicationReturned, + GymApplicationSent, } from "../validationSchema/gymApplication"; export async function getGymApplication(): Promise<GymApplicationReturned[]> { @@ -8,25 +9,46 @@ export async function getGymApplication(): Promise<GymApplicationReturned[]> { const myPromise = new Promise<GymApplicationFetched[]>(function (myResolve) { myResolve([ { - acceptance: true, + acceptance: 0, application_description: "This is an application", gym_name: "BruhBruh", + gym_id: 1, }, { - acceptance: true, + acceptance: 0, application_description: "This is an application as well", gym_name: "BruhBruhBruh", + gym_id: 2, }, ]); }); return (await myPromise).map( - ({ acceptance, application_description, gym_name }) => { + ({ acceptance, application_description, gym_name, gym_id }) => { return { acceptance, description: application_description, gymName: gym_name, + gymId: gym_id, }; } ); } + +export async function applyToGym({ + application_description, + gym_id, + gym_name, +}: GymApplicationSent): Promise<GymApplicationReturned> { + // return (await axios.get(`${config.NODE_JS_API}/api/gym/`, header)).data; + const myPromise = new Promise<GymApplicationReturned>(function (myResolve) { + myResolve({ + acceptance: 0, + description: application_description, + gymId: gym_id, + gymName: gym_name, + }); + }); + + return myPromise; +} diff --git a/src/utils/context/GymApplicationProvider.tsx b/src/utils/context/GymApplicationProvider.tsx index 6cca1d30fc92758a25adde898f32f9cfdefcc2e2..a549a6b1c8d437ee0d5ea2afc50822a9357bd398 100644 --- a/src/utils/context/GymApplicationProvider.tsx +++ b/src/utils/context/GymApplicationProvider.tsx @@ -15,7 +15,7 @@ export function useGymApplications() { return useContext(GymApplicationsContext); } -function GymProvider({ children }: { children: ReactNode }) { +function GymApplicationProvider({ children }: { children: ReactNode }) { const { status: gymApplicationsStatus, data: gymApplications } = useQuery( ["applications"], getGymApplication, @@ -37,4 +37,4 @@ function GymProvider({ children }: { children: ReactNode }) { ); } -export default GymProvider; +export default GymApplicationProvider; diff --git a/src/utils/context/GymProvider.tsx b/src/utils/context/GymProvider.tsx index e23e0bc708a9ec91a85e1bf18c803ac5f4c02390..c7d85f501e4f288a119a41140f98592519ab85bd 100644 --- a/src/utils/context/GymProvider.tsx +++ b/src/utils/context/GymProvider.tsx @@ -23,7 +23,16 @@ export function useAllGyms() { export function useGym(id: number | null) { return useQuery( ["gyms", String(id)], - async () => (id ? await getGymById({ id }) : undefined), + async () => { + if (!id) { + return undefined; + } + try { + return await getGymById({ id }); + } catch (error) { + return undefined; + } + }, { cacheTime: 300000, retry: 3, diff --git a/src/utils/validationSchema/gymApplication.ts b/src/utils/validationSchema/gymApplication.ts index af74d6317f356c43c69c5827723b2363e87f863a..6214f6129ad2310105476415ba6107f26c2a4f5b 100644 --- a/src/utils/validationSchema/gymApplication.ts +++ b/src/utils/validationSchema/gymApplication.ts @@ -2,17 +2,30 @@ import { z } from "zod"; const gymApplicationFetchedSchema = z.object({ gym_name: z.string(), - acceptance: z.boolean(), + gym_id: z.number(), + acceptance: z.number().max(2), application_description: z.string(), }); const gymApplicationReturnedSchema = z.object({ gymName: z.string(), - acceptance: z.boolean(), + gymId: z.number(), + acceptance: z.number().max(2), description: z.string(), }); +const gymApplicationSentSchema = gymApplicationFetchedSchema + .extend({ + trainer_name: z.string().max(50), + trainer_description: z.string().max(255), + username: z.string().max(50), + }) + .omit({ + acceptance: true, + }); + export type GymApplicationFetched = z.infer<typeof gymApplicationFetchedSchema>; export type GymApplicationReturned = z.infer< typeof gymApplicationReturnedSchema >; +export type GymApplicationSent = z.infer<typeof gymApplicationSentSchema>;