From ff52d7e03dd5dd18bbaf64a2efbbeb605b5ee2e6 Mon Sep 17 00:00:00 2001 From: Ahmed Ibrahim Date: Sun, 15 Dec 2024 21:29:09 +0200 Subject: [PATCH] refactor(ui): refactor book lesson logic --- .../src/components/Lessons/BookLesson.tsx | 14 +- apps/nova/src/components/Tutors/Content.tsx | 34 +- apps/nova/src/pages/TutorProfile.tsx | 10 +- packages/atlas/src/rule.ts | 14 +- packages/headless/src/lessons.ts | 13 +- packages/headless/src/rule.ts | 4 +- .../luna/src/components/Dialog/V2/Dialog.tsx | 3 +- .../BookLesson/BookLessonDialog.stories.tsx | 30 +- .../Lessons/BookLesson/BookLessonDialog.tsx | 301 +++++++++--------- packages/luna/src/components/Toast/Toast.tsx | 2 +- .../luna/src/components/Toast/provider.tsx | 2 +- .../luna/src/components/Tooltip/Tooltip.tsx | 2 +- .../src/components/TutorCard/TutorCard.tsx | 21 +- packages/luna/src/locales/ar-eg.json | 2 +- packages/luna/tailwind.config.js | 2 + packages/types/src/rule.ts | 32 +- packages/types/src/tutor.ts | 2 +- 17 files changed, 269 insertions(+), 219 deletions(-) diff --git a/apps/nova/src/components/Lessons/BookLesson.tsx b/apps/nova/src/components/Lessons/BookLesson.tsx index a2c158296..b995ce3f9 100644 --- a/apps/nova/src/components/Lessons/BookLesson.tsx +++ b/apps/nova/src/components/Lessons/BookLesson.tsx @@ -6,6 +6,8 @@ import dayjs from "dayjs"; import { useCallback } from "react"; import { useToast } from "@litespace/luna/Toast"; import { useFormatMessage } from "@litespace/luna/hooks/intl"; +import { useQueryClient } from "@tanstack/react-query"; +import { QueryKey } from "@litespace/headless/constants"; const BookLesson = ({ open, @@ -25,6 +27,7 @@ const BookLesson = ({ const toast = useToast(); const before = dayjs().toISOString(); const after = dayjs(before).add(user.notice, "minutes").toISOString(); + const queryClient = useQueryClient(); const rulesQuery = useFindUserRulesWithSlots({ id: user.tutorId, before, @@ -32,9 +35,16 @@ const BookLesson = ({ }); const onSuccess = useCallback(() => { - toast.success({ title: intl("book-lesson.success", { tutor: user.name }) }); close(); - }, [toast, close, intl, user.name]); + toast.success({ title: intl("book-lesson.success", { tutor: user.name }) }); + queryClient.invalidateQueries({ + queryKey: [ + QueryKey.FindRulesWithSlots, + QueryKey.FindLesson, + QueryKey.FindTutors, + ], + }); + }, [toast, intl, user.name, close, queryClient]); const onError = useCallback(() => { toast.error({ title: intl("book-lesson.error") }); diff --git a/apps/nova/src/components/Tutors/Content.tsx b/apps/nova/src/components/Tutors/Content.tsx index 502a772d2..a51509f27 100644 --- a/apps/nova/src/components/Tutors/Content.tsx +++ b/apps/nova/src/components/Tutors/Content.tsx @@ -5,7 +5,7 @@ import { TutorCard } from "@litespace/luna/TutorCard"; import { asFullAssetUrl } from "@litespace/luna/backend"; import { Route } from "@/types/routes"; import { useNavigate } from "react-router-dom"; -import { AnimatePresence, motion } from "framer-motion"; +import { motion } from "framer-motion"; import { InView } from "react-intersection-observer"; import BookLesson from "@/components/Lessons/BookLesson"; @@ -49,9 +49,9 @@ const Content: React.FC<{ bio={tutor.bio} about={tutor.about} name={tutor.name} - lessonCount={20} - studentCount={20} - rating={4.5} + lessonCount={tutor.lessonCount} + studentCount={tutor.studentCount} + rating={tutor.avgRating} onBook={() => openBookingDialog(tutor)} onOpenProfile={() => navigate(profileUrl)} profileUrl={profileUrl} @@ -62,20 +62,18 @@ const Content: React.FC<{ })} - - {tutor ? ( - - ) : null} - + {tutor ? ( + + ) : null} {fetching ? : null} diff --git a/apps/nova/src/pages/TutorProfile.tsx b/apps/nova/src/pages/TutorProfile.tsx index 4ae7bd961..ce4f6b868 100644 --- a/apps/nova/src/pages/TutorProfile.tsx +++ b/apps/nova/src/pages/TutorProfile.tsx @@ -1,6 +1,6 @@ import { Loading } from "@litespace/luna/Loading"; import React, { useCallback, useMemo, useState } from "react"; -import { useParams } from "react-router-dom"; +import { Link, useParams } from "react-router-dom"; import { useFindTutorInfo } from "@litespace/headless/tutor"; import RightArrow from "@litespace/assets/ArrowRight"; import { Typography } from "@litespace/luna/Typography"; @@ -8,6 +8,7 @@ import { useFormatMessage } from "@litespace/luna/hooks/intl"; import { TutorProfileCard } from "@litespace/luna/TutorProfile"; import { TutorTabs } from "@/components/TutorProfile/TutorTabs"; import BookLesson from "@/components/Lessons/BookLesson"; +import { Route } from "@/types/routes"; const TutorProfile: React.FC = () => { const params = useParams<{ id: string }>(); @@ -30,9 +31,12 @@ const TutorProfile: React.FC = () => { return (
- + {intl("tutors.title")} /{" "} {tutor.data.name} diff --git a/packages/atlas/src/rule.ts b/packages/atlas/src/rule.ts index dc8e73716..e9638d444 100644 --- a/packages/atlas/src/rule.ts +++ b/packages/atlas/src/rule.ts @@ -14,16 +14,16 @@ export class Rule extends Base { return await this.get(`/api/v1/rule/list/${id}`); } - /** - * @param id user id (manger or tutor) - * @param after utc after datetime (get rules after this date) - * @param before utc before datetime (get rules before this date) - */ async findUserRulesWithSlots({ id, - after, before, - }: IRule.FindRulesWithSlotsApiQuery): Promise { + after, + }: { + /** + * user id (manger or tutor) + */ + id: number; + } & IRule.FindRulesWithSlotsApiQuery): Promise { return await this.get(`/api/v1/rule/slots/${id}`, {}, { after, before }); } diff --git a/packages/headless/src/lessons.ts b/packages/headless/src/lessons.ts index b3906d248..1ee3dd020 100644 --- a/packages/headless/src/lessons.ts +++ b/packages/headless/src/lessons.ts @@ -1,13 +1,11 @@ -import { Element, IFilter, ILesson, Void } from "@litespace/types"; +import { Element, IFilter, ILesson } from "@litespace/types"; import { useCallback } from "react"; import { useAtlas } from "@/atlas/index"; import { MutationKey, QueryKey } from "@/constants"; import { useMutation } from "@tanstack/react-query"; import { UsePaginateResult, usePaginate } from "@/pagination"; import { InfiniteQueryHandler, useInfinitePaginationQuery } from "./query"; - -type OnSuccess = Void; -type OnError = (error: Error) => void; +import { OnError, OnSuccess } from "@/types/query"; export function useFindLessons( query: ILesson.FindLessonsApiQuery & { userOnly?: boolean } @@ -77,7 +75,7 @@ export function useCreateLesson({ onError, }: { tutorId: number; - onSuccess: OnSuccess; + onSuccess: OnSuccess; onError: OnError; }) { const atlas = useAtlas(); @@ -88,11 +86,10 @@ export function useCreateLesson({ start, duration, }: { - ruleId: number | null; - start: string | null; + ruleId: number; + start: string; duration: ILesson.Duration; }) => { - if (!ruleId || !start) return; return await atlas.lesson.create({ tutorId, duration, diff --git a/packages/headless/src/rule.ts b/packages/headless/src/rule.ts index b182c2e43..63e71b828 100644 --- a/packages/headless/src/rule.ts +++ b/packages/headless/src/rule.ts @@ -101,7 +101,7 @@ export function useDeleteRule({ } export function useFindUserRulesWithSlots( - payload: IRule.FindRulesWithSlotsApiQuery + payload: { id: number } & IRule.FindRulesWithSlotsApiQuery ) { const atlas = useAtlas(); @@ -111,6 +111,6 @@ export function useFindUserRulesWithSlots( return useQuery({ queryFn: findRules, - queryKey: [QueryKey.FindRulesWithSlots], + queryKey: [QueryKey.FindRulesWithSlots, payload], }); } diff --git a/packages/luna/src/components/Dialog/V2/Dialog.tsx b/packages/luna/src/components/Dialog/V2/Dialog.tsx index a1910515f..7dc5576a8 100644 --- a/packages/luna/src/components/Dialog/V2/Dialog.tsx +++ b/packages/luna/src/components/Dialog/V2/Dialog.tsx @@ -10,6 +10,7 @@ import { import cn from "classnames"; import React from "react"; import X from "@litespace/assets/X"; +import { Void } from "@litespace/types"; export const Dialog: React.FC<{ trigger?: React.ReactNode; @@ -18,7 +19,7 @@ export const Dialog: React.FC<{ className?: string; open?: boolean; setOpen?: (open: boolean) => void; - close: () => void; + close: Void; description?: string; }> = ({ trigger, diff --git a/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.stories.tsx b/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.stories.tsx index 441dcc3e8..34575b475 100644 --- a/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.stories.tsx +++ b/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.stories.tsx @@ -111,6 +111,33 @@ export const LoadingRules: Story = { }, }; +export const ConfirmationLoading: Story = { + args: { + open: true, + close: identity, + tutorId: faker.number.int(), + name: faker.person.fullName(), + imageUrl: faker.image.urlPicsumPhotos({ width: 400, height: 400 }), + rules: [makeRule(1)], + slots: range(5).map((idx) => ({ + ruleId: 1, + start: dayjs + .utc() + .add(1, "day") + .set("hour", 10) + .set("minutes", 0) + .add(idx * 30, "minutes") + .toISOString(), + duration: 30, + })), + notice: 0, + confirmationLoading: true, + onBook() { + alert("Lesson booked!!"); + }, + }, +}; + export const LoadingThenShowingRules: Story = { args: { open: true, @@ -139,9 +166,10 @@ export const LoadingThenShowingRules: Story = { render: (props) => { const [loading, setIsLoading] = useState(true); useEffect(() => { - setTimeout(() => { + const id = setTimeout(() => { setIsLoading(false); }, 2_000); + return () => clearTimeout(id); }, []); return ; }, diff --git a/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.tsx b/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.tsx index c4c5122f8..bcf650ee1 100644 --- a/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.tsx +++ b/packages/luna/src/components/Lessons/BookLesson/BookLessonDialog.tsx @@ -21,23 +21,58 @@ import cn from "classnames"; import Spinner from "@litespace/assets/Spinner"; import CalendarEmpty from "@litespace/assets/CalendarEmpty"; +const Loading: React.FC<{ tutorName: string | null }> = ({ tutorName }) => { + const intl = useFormatMessage(); + return ( +
+ + {tutorName ? ( + + {intl("book-lesson.loading-rules", { tutor: tutorName })} + + ) : null} +
+ ); +}; + +const BusyTutor: React.FC<{ tutorName: string | null }> = ({ tutorName }) => { + const intl = useFormatMessage(); + return ( +
+ + + {tutorName + ? intl("book-lesson.empty-slots", { tutor: tutorName }) + : null} + +
+ ); +}; + const Animation: React.FC<{ - step?: Step; + id?: Step | "loading" | "busy-tutor"; children: React.ReactNode; -}> = ({ step, children }) => { +}> = ({ id, children }) => { const duration = useMemo(() => { - if (step === "date-selection" || step === "time-selection") return 0.5; + if (id === "date-selection" || id === "loading" || id === "busy-tutor") + return 0.5; return 0.4; - }, [step]); - - const delay = useMemo(() => { - if (step === "date-selection" || step === "time-selection") return 0.4; - return 0.2; - }, [step]); + }, [id]); return ( { - console.log(unpackedRules); - const daySlots = unpackedRules.filter( (event) => day.isSame(event.start, "day") || day.isSame(event.end, "day") @@ -199,9 +231,9 @@ export const BookLessonDialog: React.FC<{ close={close} title={ {name @@ -211,151 +243,122 @@ export const BookLessonDialog: React.FC<{ } className="!tw-p-0 !tw-pt-6 !tw-pb-3 [&>div:first-child]:!tw-px-6" > - - {loading ? ( - -
- - - {intl("book-lesson.loading-rules", { tutor: name })} - -
-
- ) : null} -
- {!loading ? ( - <> -
- -
+
+ +
+ ) : null} -
- - {isTutorBusy ? ( - -
- - - {intl("book-lesson.empty-slots", { tutor: name })} - -
-
- ) : null} -
+
+ + {loading ? ( + + + + ) : null} - - {!isTutorBusy && step === "date-selection" ? ( - - - - ) : null} - + {isTutorBusy ? ( + + + + ) : null} - - {!isTutorBusy && step === "duration-selection" ? ( - -
- -
-
- ) : null} -
+ {step === "date-selection" && !loading && !isTutorBusy ? ( + + + + ) : null} - - {!isTutorBusy && step === "time-selection" ? ( - - { - setStart(start); - setRuleId(ruleId); - }} - /> - - ) : null} - + {!isTutorBusy && step === "duration-selection" && !loading ? ( + +
+ +
+
+ ) : null} - - {!isTutorBusy && step === "confirmation" && start && ruleId ? ( - -
- onBook({ ruleId, start, duration })} - onEdit={() => { - setStep("date-selection"); - }} - /> -
-
- ) : null} -
-
+ {!isTutorBusy && step === "time-selection" && !loading ? ( + + { + setStart(start); + setRuleId(ruleId); + }} + /> + + ) : null} - {step !== "confirmation" ? ( -
- {step !== "date-selection" ? ( - - ) : null} + /> +
+ + ) : null} + +
- -
+ {step !== "confirmation" && !loading && !isTutorBusy ? ( +
+ {step !== "date-selection" ? ( + ) : null} - + + +
) : null} ); diff --git a/packages/luna/src/components/Toast/Toast.tsx b/packages/luna/src/components/Toast/Toast.tsx index ffd2c8910..47342a261 100644 --- a/packages/luna/src/components/Toast/Toast.tsx +++ b/packages/luna/src/components/Toast/Toast.tsx @@ -50,7 +50,7 @@ export const Toast: React.FC<{
= ({ diff --git a/packages/luna/src/components/Tooltip/Tooltip.tsx b/packages/luna/src/components/Tooltip/Tooltip.tsx index a818dffc1..c3ad8fffa 100644 --- a/packages/luna/src/components/Tooltip/Tooltip.tsx +++ b/packages/luna/src/components/Tooltip/Tooltip.tsx @@ -22,7 +22,7 @@ export const Tooltip: React.FC<{ {content} diff --git a/packages/luna/src/components/TutorCard/TutorCard.tsx b/packages/luna/src/components/TutorCard/TutorCard.tsx index 071b7c457..d5654ef7e 100644 --- a/packages/luna/src/components/TutorCard/TutorCard.tsx +++ b/packages/luna/src/components/TutorCard/TutorCard.tsx @@ -14,6 +14,7 @@ import { formatNumber } from "@/components/utils"; import { Link } from "react-router-dom"; import { orUndefined } from "@litespace/sol/utils"; import { Void } from "@litespace/types"; +import { Tooltip } from "@/components/Tooltip"; type Props = { id: number; @@ -60,13 +61,21 @@ export const TutorCard: React.FC = ({ />
- {name} + } > - {name} - +
+ + {name} + +
+ ; /** @@ -142,3 +125,18 @@ export type Slot = { */ duration: number; }; + +export type FindRulesWithSlotsApiQuery = { + /** + * ISO UTC datetime. + * + * Get user rules that are defined after this date. + */ + after: string; + /** + * ISO UTC datetime. + * + * Get user rules that are defined before this date. + */ + before: string; +}; diff --git a/packages/types/src/tutor.ts b/packages/types/src/tutor.ts index 3bb9b38f2..94cd1648a 100644 --- a/packages/types/src/tutor.ts +++ b/packages/types/src/tutor.ts @@ -8,7 +8,7 @@ export type Self = { activated: boolean | null; activatedBy: number | null; /** - * time before the student is able to book a lesson with the tutor in *minutes* + * The period that be available before booking any lesson with the tutor. */ notice: number; createdAt: string;