-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
merge:update local br from remote one
- Loading branch information
Showing
32 changed files
with
2,082 additions
and
113 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
module.exports = { | ||
presets: [ | ||
["@babel/preset-env", { targets: { node: "current" } }], | ||
"@babel/preset-typescript", | ||
], | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
/** @type {import('ts-jest').JestConfigWithTsJest} **/ | ||
module.exports = { | ||
testEnvironment: "node", | ||
transformIgnorePatterns: ["<rootDir>/node_modules/"], | ||
transform: { | ||
"^.+\\.(ts|tsx)?$": [ | ||
"ts-jest", | ||
{ | ||
diagnostics: { | ||
exclude: ["**"], | ||
}, | ||
}, | ||
], | ||
"^.+\\.(js|jsx)$": "babel-jest", | ||
}, | ||
moduleNameMapper: { | ||
"^@/(.*)$": "<rootDir>/src/$1", | ||
}, | ||
}; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import { | ||
Button, | ||
ButtonSize, | ||
ButtonType, | ||
ButtonVariant, | ||
} from "@litespace/luna/Button"; | ||
import { Dialog } from "@litespace/luna/Dialog"; | ||
import { Field, Label } from "@litespace/luna/Form"; | ||
import { Select } from "@litespace/luna/Select"; | ||
import { useToast } from "@litespace/luna/Toast"; | ||
import { useFormatMessage } from "@litespace/luna/hooks/intl"; | ||
import { Schedule, splitRuleEvent } from "@litespace/sol/rule"; | ||
import { ILesson, IRule } from "@litespace/types"; | ||
import { entries, flattenDeep, groupBy } from "lodash"; | ||
import React, { useCallback, useMemo, useState } from "react"; | ||
import dayjs from "@/lib/dayjs"; | ||
import { useCreateLesson } from "@litespace/headless/lessons"; | ||
|
||
const BookLesson: React.FC<{ | ||
open: boolean; | ||
close: () => void; | ||
tutorId: number; | ||
name: string; | ||
rules: IRule.RuleEvent[]; | ||
notice: number; | ||
}> = ({ open, close, tutorId, name, rules, notice }) => { | ||
const intl = useFormatMessage(); | ||
const toast = useToast(); | ||
const [duration, setDuration] = useState<ILesson.Duration>( | ||
ILesson.Duration.Long | ||
); | ||
const [selectedEvent, setSelectedEvent] = useState<IRule.RuleEvent | null>( | ||
null | ||
); | ||
|
||
const events = useMemo(() => { | ||
const events = Schedule.order( | ||
flattenDeep(rules.map((rule) => splitRuleEvent(rule, duration))).filter( | ||
(event) => dayjs(event.start).isAfter(dayjs().add(notice, "minutes")) | ||
), | ||
"asc" | ||
); | ||
|
||
const map = groupBy(events, (event) => | ||
dayjs(event.start).format("YYYY-MM-DD") | ||
); | ||
|
||
return entries(map); | ||
}, [duration, notice, rules]); | ||
|
||
const onClose = useCallback(() => { | ||
setSelectedEvent(null); | ||
close(); | ||
}, [close]); | ||
|
||
const onSuccess = useCallback(() => { | ||
toast.success({ | ||
title: intl("page.tutors.book.lesson.success", { tutor: name }), | ||
}); | ||
|
||
onClose(); | ||
}, [intl, name, onClose, toast]); | ||
|
||
const onError = useCallback( | ||
(error: unknown) => { | ||
toast.error({ | ||
title: intl("page.tutors.book.lesson.error"), | ||
description: error instanceof Error ? error.message : undefined, | ||
}); | ||
}, | ||
[intl, toast] | ||
); | ||
|
||
const mutation = useCreateLesson({ | ||
selectedEvent, | ||
tutorId, | ||
duration, | ||
onSuccess, | ||
onError, | ||
}); | ||
|
||
const options = useMemo(() => { | ||
return [ | ||
{ | ||
label: intl("global.lesson.duration.15"), | ||
value: 15, | ||
}, | ||
{ | ||
label: intl("global.lesson.duration.30"), | ||
value: 30, | ||
}, | ||
]; | ||
}, [intl]); | ||
|
||
return ( | ||
<Dialog | ||
title={intl("page.tutors.book.lesson.dialog.title", { name })} | ||
open={open} | ||
close={onClose} | ||
className="w-full md:w-3/4 max-w-[40rem]" | ||
> | ||
<Field | ||
label={ | ||
<Label> | ||
{intl("page.tutors.book.lesson.dialog.lesson.duration")} | ||
</Label> | ||
} | ||
field={ | ||
<Select value={duration} options={options} onChange={setDuration} /> | ||
} | ||
/> | ||
|
||
<ul className="h-[500px] overflow-y-auto scrollbar-thin flex flex-col gap-3 pb-4 pl-1.5 mt-4 "> | ||
{events.map(([date, list]) => ( | ||
<li key={date} className="text-foreground"> | ||
<h3 className="mb-3 text-base font-semibold"> | ||
{dayjs(date).format("dddd, DD MMMM")} | ||
</h3> | ||
<ul className="grid grid-cols-12 gap-4"> | ||
{list.map((event) => ( | ||
<li key={event.start} className="col-span-3"> | ||
<Button | ||
onClick={() => setSelectedEvent(event)} | ||
disabled={mutation.isPending} | ||
type={ButtonType.Main} | ||
variant={ | ||
selectedEvent?.start === event.start | ||
? ButtonVariant.Primary | ||
: ButtonVariant.Secondary | ||
} | ||
className="w-full" | ||
> | ||
{dayjs(event.start).format("h:mm a")} | ||
</Button> | ||
</li> | ||
))} | ||
</ul> | ||
</li> | ||
))} | ||
</ul> | ||
|
||
<Button | ||
disabled={!selectedEvent || mutation.isPending} | ||
loading={mutation.isPending} | ||
onClick={() => mutation.mutate()} | ||
className="mt-4" | ||
size={ButtonSize.Small} | ||
> | ||
{intl("global.labels.confirm")} | ||
</Button> | ||
</Dialog> | ||
); | ||
}; | ||
|
||
export default BookLesson; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import { Alert } from "@litespace/luna/Alert"; | ||
import { Spinner } from "@litespace/luna/Spinner"; | ||
import { useFormatMessage } from "@litespace/luna/hooks/intl"; | ||
import { Element, ITutor, Wss } from "@litespace/types"; | ||
import { isEmpty } from "lodash"; | ||
import React, { useCallback, useEffect, useMemo, useState } from "react"; | ||
import { UseQueryResult } from "@tanstack/react-query"; | ||
import { useNavigate } from "react-router-dom"; | ||
import TutorCard from "@/components/Tutors/TutorCard"; | ||
import BookLesson from "@/components/Tutors/BookLesson"; | ||
import { useSocket } from "@litespace/headless/socket"; | ||
|
||
type Tutor = Element<ITutor.FindOnboardedTutorsApiResponse["list"]>; | ||
|
||
const TutorList: React.FC<{ | ||
tutors: UseQueryResult<ITutor.FindOnboardedTutorsApiResponse, unknown>; | ||
}> = ({ tutors }) => { | ||
const socket = useSocket(); | ||
const intl = useFormatMessage(); | ||
const navigate = useNavigate(); | ||
const [tutor, setTutor] = useState<Tutor | null>(null); | ||
const select = useCallback((tutor: Tutor) => setTutor(tutor), []); | ||
const deselect = useCallback(() => setTutor(null), []); | ||
|
||
const reload = useMemo(() => { | ||
return { | ||
label: intl("global.labels.reload.page"), | ||
onClick() { | ||
navigate(0); | ||
}, | ||
}; | ||
}, [intl, navigate]); | ||
|
||
const onUpdate = useCallback(async () => { | ||
const { data } = await tutors.refetch(); | ||
if (!data || !tutor) return; | ||
const updated = data.list.find((t) => t.id === tutor.id); | ||
setTutor(updated || null); | ||
}, [tutor, tutors]); | ||
|
||
useEffect(() => { | ||
if (!socket) return; | ||
|
||
socket.on(Wss.ServerEvent.TutorUpdated, onUpdate); | ||
socket.on(Wss.ServerEvent.LessonBooked, onUpdate); | ||
socket.on(Wss.ServerEvent.LessonCanceled, onUpdate); | ||
socket.on(Wss.ServerEvent.RuleCreated, onUpdate); | ||
socket.on(Wss.ServerEvent.RuleUpdated, onUpdate); | ||
socket.on(Wss.ServerEvent.RuleDeleted, onUpdate); | ||
return () => { | ||
socket.off(Wss.ServerEvent.TutorUpdated, onUpdate); | ||
socket.off(Wss.ServerEvent.LessonBooked, onUpdate); | ||
socket.off(Wss.ServerEvent.LessonCanceled, onUpdate); | ||
socket.off(Wss.ServerEvent.RuleCreated, onUpdate); | ||
socket.off(Wss.ServerEvent.RuleUpdated, onUpdate); | ||
socket.off(Wss.ServerEvent.RuleDeleted, onUpdate); | ||
}; | ||
}, [onUpdate, socket]); | ||
|
||
if (tutors.isLoading) | ||
return ( | ||
<div className="flex items-center justify-center h-[500px]"> | ||
<Spinner /> | ||
</div> | ||
); | ||
|
||
if (tutors.isError) | ||
return ( | ||
<Alert title={intl("error.tutors.list.error")} action={reload}> | ||
{tutors.error instanceof Error ? tutors.error.message : null} | ||
</Alert> | ||
); | ||
|
||
// todo: show empty search list with a nice image | ||
if (!tutors.data || isEmpty(tutors.data.list)) | ||
return <Alert title={intl("error.tutors.list.empty")} />; | ||
|
||
return ( | ||
<div className="grid grid-cols-12 gap-6"> | ||
{tutors.data.list.map((tutor) => ( | ||
<div | ||
key={tutor.id} | ||
className="col-span-12 md:col-span-6 lg:col-span-4 2xl:col-span-3" | ||
> | ||
<TutorCard tutor={tutor} select={() => select(tutor)} /> | ||
</div> | ||
))} | ||
|
||
{tutor && tutor.name ? ( | ||
<BookLesson | ||
open={!!tutor} | ||
close={deselect} | ||
name={tutor.name} | ||
rules={tutor.rules} | ||
tutorId={tutor.id} | ||
notice={tutor.notice} | ||
/> | ||
) : null} | ||
</div> | ||
); | ||
}; | ||
|
||
export default TutorList; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
import { Route } from "@/types/routes"; | ||
import { Button, ButtonSize } from "@litespace/luna/Button"; | ||
import { asFullAssetUrl } from "@litespace/luna/backend"; | ||
import { useFormatMessage } from "@litespace/luna/hooks/intl"; | ||
import { ITutor } from "@litespace/types"; | ||
import cn from "classnames"; | ||
import React from "react"; | ||
import { Link } from "react-router-dom"; | ||
|
||
const TutorCard: React.FC<{ | ||
tutor: ITutor.Cache; | ||
select: () => void; | ||
}> = ({ tutor, select }) => { | ||
const intl = useFormatMessage(); | ||
|
||
return ( | ||
<div | ||
className={cn( | ||
"rounded-lg overflow-hidden shadow-2xl bg-surface-100", | ||
"border border-border hover:border-border-stronger", | ||
"transition-colors duration-300" | ||
)} | ||
> | ||
<div className="h-[300px] md:h-[400px]"> | ||
<img | ||
className="object-cover w-full h-full" | ||
src={tutor.image ? asFullAssetUrl(tutor.image) : "/avatar-1.png"} | ||
alt={tutor.name!} | ||
/> | ||
</div> | ||
|
||
<div className="flex flex-row items-center justify-between p-4"> | ||
<div className="w-3/4"> | ||
<Link | ||
to={Route.TutorProfile.replace(":id", tutor.id.toString())} | ||
className="underline text-brand-link" | ||
> | ||
<h6 className="mb-1">{tutor.name}</h6> | ||
</Link> | ||
<p className="text-sm truncate text-foreground-light">{tutor.bio}</p> | ||
</div> | ||
|
||
<div> | ||
<Button onClick={select} size={ButtonSize.Small}> | ||
{intl("global.book.lesson.label")} | ||
</Button> | ||
</div> | ||
</div> | ||
</div> | ||
); | ||
}; | ||
|
||
export default TutorCard; |
Oops, something went wrong.