Skip to content

Commit

Permalink
Merge pull request #231 from litespace-org/mk/student-dashboard
Browse files Browse the repository at this point in the history
feat(ui): implemented full student dashboard
  • Loading branch information
neuodev authored Dec 26, 2024
2 parents 6684773 + d20a776 commit 2ba19a3
Show file tree
Hide file tree
Showing 22 changed files with 617 additions and 164 deletions.
2 changes: 1 addition & 1 deletion apps/nova/src/components/Layout/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const Sidebar = () => {
return [
{
label: intl("sidebar.dashboard"),
route: Route.Root,
route: Route.Dashboard,
Icon: Home,
},
{
Expand Down
43 changes: 43 additions & 0 deletions apps/nova/src/components/dashboard/ChatSummary.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Route } from "@/types/routes";
import { useFindUserRooms } from "@litespace/headless/chat";
import { useUser } from "@litespace/headless/context/user";
import {
ChatSummary as Summary,
type ChatSummaryProps,
} from "@litespace/luna/Chat";
import { orUndefined } from "@litespace/sol/utils";
import { IRoom } from "@litespace/types";
import dayjs from "dayjs";
import { useMemo } from "react";

function asRooms(
list: IRoom.FindUserRoomsApiRecord[] | null
): ChatSummaryProps["rooms"] {
if (!list) return [];

return list.map((room) => ({
id: room.roomId,
url: Route.Chat.concat("?room=", room.roomId.toString()),
name: orUndefined(room.otherMember?.name),
image: orUndefined(room.otherMember?.image),
message: room.latestMessage?.text || "TODO",
sentAt: room.latestMessage?.updatedAt || dayjs().toString(), // TODO
read: room.unreadMessagesCount === 0,
}));
}

export const ChatSummary = () => {
const { user } = useUser();
const rooms = useFindUserRooms(user?.id, { size: 4 });
const organizedRooms = useMemo(() => asRooms(rooms.list), [rooms.list]);

return (
<Summary
loading={rooms.query.isPending}
error={rooms.query.isError}
retry={rooms.query.refetch}
chatsUrl={Route.Chat}
rooms={organizedRooms}
/>
);
};
101 changes: 101 additions & 0 deletions apps/nova/src/components/dashboard/PastLessons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { Route } from "@/types/routes";
import { useUser } from "@litespace/headless/context/user";
import { useInfiniteLessons } from "@litespace/headless/lessons";
import { useFormatMessage } from "@litespace/luna/hooks/intl";
import { PastLessonsTable } from "@litespace/luna/Lessons";
import { Typography } from "@litespace/luna/Typography";
import { ILesson, IUser } from "@litespace/types";
import { useCallback, useMemo, useState } from "react";
import BookLesson from "@/components/Lessons/BookLesson";
import { InView } from "react-intersection-observer";
import dayjs from "dayjs";
import { Loading } from "@litespace/luna/Loading";

function asLessons(list: ILesson.FindUserLessonsApiResponse["list"] | null) {
if (!list) return [];

return list.map((lesson) => {
const teacher = lesson.members.find(
(member) => member.role === IUser.Role.Tutor
);
return {
id: lesson.lesson.id,
start: lesson.lesson.start,
duration: lesson.lesson.duration,
tutor: {
id: teacher?.userId || 0,
name: teacher?.name || null,
imageUrl: teacher?.image || null,
},
};
});
}

export const PastLessons = () => {
const intl = useFormatMessage();
const [tutor, setTutor] = useState<number | null>(null);

const closeRebookingDialog = useCallback(() => {
setTutor(null);
}, []);

const openRebookingDialog = useCallback((tutorId: number) => {
setTutor(tutorId);
}, []);

const { user } = useUser();
const now = useMemo(() => dayjs.utc().toISOString(), []);

const lessonsQuery = useInfiniteLessons({
users: user ? [user?.id] : [],
userOnly: true,
before: now,
});

const lessons = useMemo(
() => asLessons(lessonsQuery.list),
[lessonsQuery.list]
);

return (
<div className="grid gap-6 ">
<Typography
element="subtitle-2"
weight="bold"
className="text-natural-950 "
>
{intl("student-dashboard.previous-lessons.title")}
</Typography>

<PastLessonsTable
tutorsRoute={Route.Tutors}
onRebook={openRebookingDialog}
lessons={lessons}
loading={lessonsQuery.query.isPending}
error={lessonsQuery.query.isError}
retry={lessonsQuery.query.refetch}
/>

{!lessonsQuery.query.isFetching && lessonsQuery.query.hasNextPage ? (
<InView
as="div"
onChange={(inView) => {
if (inView && lessonsQuery.query.hasNextPage) lessonsQuery.more();
}}
/>
) : null}

{lessonsQuery.query.isFetching && !lessonsQuery.query.isLoading ? (
<Loading />
) : null}

{tutor ? (
<BookLesson
close={closeRebookingDialog}
open={!!tutor}
tutorId={tutor}
/>
) : null}
</div>
);
};
32 changes: 32 additions & 0 deletions apps/nova/src/components/dashboard/StudentOverview.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useFormatMessage } from "@litespace/luna/hooks/intl";
import { StudentOverview as Overview } from "@litespace/luna/StudentOverview";
import { Typography } from "@litespace/luna/Typography";
import { useFindPublicStudentStats } from "@litespace/headless/student";

export const StudentOverview = () => {
const intl = useFormatMessage();

const statsQuery = useFindPublicStudentStats();

return (
<div className="grid gap-6 justify-items-start w-full">
<Typography
element="subtitle-2"
weight="bold"
className="text-natural-950"
>
{intl("student-dashboard.overview.title")}
</Typography>

<Overview
tutorCount={statsQuery.data?.tutorCount || 0}
completedLessonCount={statsQuery.data?.completedLessonCount || 0}
totalLearningTime={statsQuery.data?.totalLearningTime || 0}
totalLessonCount={statsQuery.data?.completedLessonCount || 0}
loading={statsQuery.isPending}
error={statsQuery.isError}
retry={statsQuery.refetch}
/>
</div>
);
};
47 changes: 47 additions & 0 deletions apps/nova/src/components/dashboard/UpcomingLessons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Route } from "@/types/routes";
import { useUser } from "@litespace/headless/context/user";
import { useInfiniteLessons } from "@litespace/headless/lessons";
import { UpcomingLessonsSummary as Summary } from "@litespace/luna/Lessons";
import { ILesson, IUser } from "@litespace/types";
import dayjs from "dayjs";
import { useMemo } from "react";

function asUpcomingLessons(
list: ILesson.FindUserLessonsApiResponse["list"] | null
) {
if (!list) return [];

return list.map((item) => ({
start: item.lesson.start,
tutorName:
item.members.find((member) => member.role === IUser.Role.Tutor)?.name ||
null,
url: Route.Call,
}));
}

export const UpcomingLessons = () => {
const { user } = useUser();

const now = useMemo(() => dayjs.utc().toISOString(), []);
const lessonsQuery = useInfiniteLessons({
users: user ? [user?.id] : [],
userOnly: true,
after: now,
size: 4,
});
const lessons = useMemo(
() => asUpcomingLessons(lessonsQuery.list),
[lessonsQuery.list]
);
return (
<Summary
loading={lessonsQuery.query.isPending}
error={lessonsQuery.query.isError}
retry={lessonsQuery.query.refetch}
lessons={lessons}
lessonsUrl={Route.UpcomingLessons}
tutorsUrl={Route.Tutors}
/>
);
};
17 changes: 16 additions & 1 deletion apps/nova/src/pages/Dashboard.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,22 @@
import React from "react";
import { ChatSummary } from "@/components/dashboard/ChatSummary";
import { UpcomingLessons } from "@/components/dashboard/UpcomingLessons";
import { PastLessons } from "@/components/dashboard/PastLessons";
import { StudentOverview } from "@/components/dashboard/StudentOverview";

const Dashboard: React.FC = () => {
return <div>Dashboard</div>;
return (
<div className="flex flex-row p-6 gap-6 max-w-screen-3xl mx-auto w-full">
<div className="flex flex-col gap-6 w-full">
<StudentOverview />
<PastLessons />
</div>
<div className="flex flex-col gap-6 w-[312px] shrink-0">
<UpcomingLessons />
<ChatSummary />
</div>
</div>
);
};

export default Dashboard;
29 changes: 20 additions & 9 deletions packages/assets/assets/people.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions packages/headless/src/constants/query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export enum QueryKey {
FindWithdrawalMethods = "find-withdrawal-methods",
FindUserById = "find-user-by-id",
FindStudentStats = "find-student-stats",
FindPublicStudentStats = "find-public-student-stats",
FindTopic = "find-topic",
FindPeerId = "find-peer-id",
FindAsset = "find-asset",
Expand Down
13 changes: 13 additions & 0 deletions packages/headless/src/student.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,16 @@ export function useFindStudentStats(
enabled: !!id,
});
}

export function useFindPublicStudentStats() {
const atlas = useAtlas();

const findStats = useCallback(async () => {
return await atlas.user.findPublicStudentStats();
}, [atlas.user]);

return useQuery({
queryFn: findStats,
queryKey: [QueryKey.FindPublicStudentStats],
});
}
1 change: 1 addition & 0 deletions packages/luna/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"./SidebarNav": "./dist/components/SidebarNav/index.js",
"./Slider": "./dist/components/Slider/index.js",
"./Stepper": "./dist/components/Stepper/index.js",
"./StudentOverview": "./dist/components/StudentOverview/index.js",
"./Switch": "./dist/components/Switch/index.js",
"./Textarea": "./dist/components/Textarea/index.js",
"./TextEditor": "./dist/components/TextEditor/index.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const makeRoom = () => ({
url: "/",
name: faker.person.fullName(),
image: faker.image.urlPicsumPhotos({ width: 400, height: 400 }),
message: faker.lorem.words(10),
message: faker.lorem.words(1),
sentAt: faker.date.past().toISOString(),
read: faker.datatype.boolean(),
});
Expand All @@ -37,6 +37,23 @@ export const Primary: Story = {
},
};

export const Loading: Story = {
args: {
rooms: range(4).map(() => makeRoom()),
chatsUrl: "/",
loading: true,
},
};

export const Error: Story = {
args: {
rooms: range(4).map(() => makeRoom()),
chatsUrl: "/",
error: true,
retry: () => alert("retry"),
},
};

export const Empty: Story = {
args: {
rooms: [],
Expand Down
Loading

0 comments on commit 2ba19a3

Please sign in to comment.