Skip to content

Commit

Permalink
merge:update local br from remote one
Browse files Browse the repository at this point in the history
  • Loading branch information
moalidv committed Dec 22, 2024
2 parents 5247579 + 5f928ce commit 52e94f6
Show file tree
Hide file tree
Showing 32 changed files with 2,082 additions and 113 deletions.
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/ServerStats/Chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const Chart = <T extends { timestamp: number }>({
dataKey?: keyof T;
}) => {
return (
<div className="h-[20rem] shadow-ls-small rounded-md p-4" dir="ltr">
<div className="h-[20rem] shadow-ls-x-small rounded-md p-4" dir="ltr">
<ResponsiveContainer>
<LineChart data={data}>
{lines.map((key) => (
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/components/UserDetails/Content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const Content: React.FC<{
if (!user) return;

return (
<div className="p-4 mx-auto mt-6 border border-border-strong rounded-md shadow-ls-small w-full">
<div className="p-4 mx-auto mt-6 border border-border-strong rounded-md shadow-ls-x-small w-full">
<div className="flex gap-2 ">
<div className="relative">
{user.image ? (
Expand Down
2 changes: 1 addition & 1 deletion apps/dashboard/src/pages/PlatformSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Cache from "@/components/Settings/Cache";

const Settings = () => {
return (
<div className="w-full flex flex-col max-w-screen-2xl mx-auto m-6 p-6 shadow-ls-small rounded-md h-fit">
<div className="w-full flex flex-col max-w-screen-2xl mx-auto m-6 p-6 shadow-ls-x-small rounded-md h-fit">
<Cache />
</div>
);
Expand Down
6 changes: 6 additions & 0 deletions apps/nova/babel.config.cjs
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",
],
};
19 changes: 19 additions & 0 deletions apps/nova/jest.config.cjs
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",
},
};
155 changes: 155 additions & 0 deletions apps/nova/src/components/Tutors/BookLesson.tsx
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;
103 changes: 103 additions & 0 deletions apps/nova/src/components/Tutors/List.tsx
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;
53 changes: 53 additions & 0 deletions apps/nova/src/components/Tutors/TutorCard.tsx
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;
Loading

0 comments on commit 52e94f6

Please sign in to comment.