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" ? (
-
}
- size={ButtonSize.Small}
- onClick={() => {
- if (step === "time-selection")
- setStep("duration-selection");
- if (step === "duration-selection")
- setStep("date-selection");
+ {!isTutorBusy &&
+ step === "confirmation" &&
+ start &&
+ ruleId &&
+ !loading ? (
+
+
+ onBook({ ruleId, start, duration })}
+ onEdit={() => {
+ setStep("date-selection");
}}
- className={cn({
- "tw-w-[128px]": step === "duration-selection",
- })}
- >
- {intl("book-lesson.steps.prev")}
-
- ) : null}
+ />
+
+
+ ) : null}
+
+
-
}
- size={ButtonSize.Small}
- onClick={() => {
- if (step === "date-selection") setStep("duration-selection");
- if (step === "duration-selection") setStep("time-selection");
- if (step === "time-selection") setStep("confirmation");
- }}
- disabled={
- (step === "time-selection" && !start) || !isValidDate(date)
- }
- className={cn({
- "tw-w-[196px]": step === "date-selection",
- "tw-w-[128px]": step === "duration-selection",
- })}
- >
- {intl("book-lesson.steps.next")}
-
-
+ {step !== "confirmation" && !loading && !isTutorBusy ? (
+
+ {step !== "date-selection" ? (
+ }
+ size={ButtonSize.Small}
+ onClick={() => {
+ if (step === "time-selection") setStep("duration-selection");
+ if (step === "duration-selection") setStep("date-selection");
+ }}
+ className={cn({
+ "tw-w-[128px]": step === "duration-selection",
+ })}
+ >
+ {intl("book-lesson.steps.prev")}
+
) : null}
- >
+
+ }
+ size={ButtonSize.Small}
+ onClick={() => {
+ if (step === "date-selection") setStep("duration-selection");
+ if (step === "duration-selection") setStep("time-selection");
+ if (step === "time-selection") setStep("confirmation");
+ }}
+ disabled={
+ (step === "time-selection" && !start) || !isValidDate(date)
+ }
+ className={cn({
+ "tw-w-[196px]": step === "date-selection",
+ "tw-w-[128px]": step === "duration-selection",
+ })}
+ >
+ {intl("book-lesson.steps.next")}
+
+
) : 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;