Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 0 additions & 9 deletions client/src/App.tsx

This file was deleted.

7 changes: 7 additions & 0 deletions client/src/api/subjects/subjects.api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { apiPublic } from "../api.ts";
import { SubjectsType } from "./subjects.type.ts";

export const getAllSubjects = async (): Promise<SubjectsType[]> => {
const response = await apiPublic.get("/api/subjects");
return response.data;
};
4 changes: 4 additions & 0 deletions client/src/api/subjects/subjects.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SubjectsType = {
id: string;
name: string;
};
8 changes: 5 additions & 3 deletions client/src/components/filters/Filters.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@ import { Checkbox } from "../ui/checkbox/Checkbox";
import { SliderRange } from "../ui/sliderRange/SliderRange";
import { useTeachersFiltersStore } from "../../store/filters.store.ts";
import { useShallow } from "zustand/react/shallow";
import { Subjects } from "../../constants/subjects.ts";
import { useSearchParams } from "react-router-dom";
type FiltersProps = {
radioGroupValues: { label: string; value: string }[];
};

export const Filters = () => {
export const Filters = ({ radioGroupValues }: FiltersProps) => {
const [, setSearchParams] = useSearchParams();
const {
subjectDraft,
Expand Down Expand Up @@ -64,7 +66,7 @@ export const Filters = () => {
>
<h5 className="text-light-100 mb-5">Tutors</h5>
<RadioGroup
options={Subjects}
options={radioGroupValues}
value={subjectDraft ?? ""}
onValueChange={(v: string) => setSubjectDraft(v || undefined)}
/>
Expand Down
51 changes: 51 additions & 0 deletions client/src/components/skeletons/FiltersSkeleton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
const Skeleton = ({ className }: { className?: string }) => (
<div
className={`
animate-pulse
bg-white/10
rounded-md
${className}
`}
/>
);

export const FiltersSkeleton = () => {
return (
<div className="bg-white/10 w-72.5 sm:w-full lg:w-78 p-5 rounded-[30px]">
<Skeleton className="h-5 w-24 mb-5" />

<div className="flex flex-col items-start py-5 mr-7.5 mb-5 border-light-100 border-b border-t">
<Skeleton className="h-4 w-20 mb-5" />

<div className="flex flex-col gap-4 w-full">
<Skeleton className="h-4 w-32" />
<Skeleton className="h-4 w-28" />
<Skeleton className="h-4 w-36" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
<Skeleton className="h-4 w-24" />
</div>
</div>

<div className="flex flex-col items-start gap-5 mb-6">
<Skeleton className="h-3 w-28" />
<Skeleton className="h-4 w-full rounded-full" />
</div>

<div className="mb-5">
<Skeleton className="h-3 w-32 mb-5" />
<div className="flex flex-col gap-4 py-5 border-light-100 border-b border-t">
{[1, 2, 3, 4, 5].map((i) => (
<Skeleton key={i} className="h-4 w-40" />
))}
</div>
</div>

<div className="flex flex-col gap-5 mb-6">
<Skeleton className="h-10 w-full rounded-xl" />
<Skeleton className="h-10 w-full rounded-xl" />
</div>
</div>
);
};
2 changes: 1 addition & 1 deletion client/src/components/teacherProfileSection/LessonForm.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Button } from "../ui/button/Button";
import { SelectComponent } from "../ui/select/select";
import { SelectComponent } from "../ui/select/Select";
import { LEVELS, SUBJECTS } from "./constants";
import { useState } from "react";
import { useModalStore } from "../../store/modals.store";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { SelectComponent } from "../ui/select/select";
import { SelectComponent } from "../ui/select/Select.tsx";
import { EXPERIENCE_OPTIONS, EDUCATION_OPTIONS } from "./constants";

type ProfileExperienceEducationProps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { TeacherType } from "../../../api/teacher/teacher.type";
import { useModalStore } from "../../../store/modals.store";
import { useAuthSessionStore } from "../../../store/authSession.store";
import { useTeacherAppointmentsQuery } from "../../../features/appointments/query/useTeacherAppointmentsQuery";
import { SelectComponent } from "../../ui/select/select";
import { SelectComponent } from "../../ui/select/Select.tsx";
import { Button } from "../../ui/button/Button";

interface TeacherScheduleProps {
Expand Down
5 changes: 4 additions & 1 deletion client/src/features/queryKeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ export const queryKeys = {
teachers: {
all: ["teachers"] as const,
myProfile: () => ["teachers", "me"] as const,
detail: (id: string) => ["teachers", id] as const,
list: (params: TeachersQuery) => ["teachers", "list", params] as const,
},
teacher: (id: string) => ["teachers", id] as const,
Expand All @@ -27,3 +26,7 @@ export const chatKeys = {
messages: (conversationId: string) =>
["chat", "messages", conversationId] as const,
};

export const subjectsKey = {
all: ["subjects"] as const,
};
24 changes: 24 additions & 0 deletions client/src/features/subjects/query/useSubjectsQuery.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { useNotificationStore } from "../../../store/notification.store.ts";
import { useQuery } from "@tanstack/react-query";
import { subjectsKey } from "../../queryKeys.ts";
import { useEffect } from "react";
import { getErrorMessage } from "../../../util/ErrorUtil.ts";
import { getAllSubjects } from "../../../api/subjects/subjects.api.ts";

export function useSubjectsQuery() {
const notifyError = useNotificationStore((s) => s.error);

const query = useQuery({
queryKey: subjectsKey.all,
queryFn: getAllSubjects,
});

useEffect(() => {
if (query.isError) {
const msg = getErrorMessage(query.error);
notifyError(msg ?? "Failed to load subjects");
}
}, [query.isError, query.isSuccess, query.error, notifyError]);

return query;
}
2 changes: 1 addition & 1 deletion client/src/pages/chat/chatDialogPage/ChatDialogPage.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { useParams } from "react-router-dom";
import { ChatSideBarItem } from "../../../components/chat/chatSidebarItem/ChatSideBarItem.tsx";
import { useMemo, useState } from "react";
import { formatDate } from "../../../util/data.util.ts";
import { formatDate } from "../../../util/date.util.ts";
import { TextField } from "../../../components/ui/textField/TextField.tsx";
import { Button } from "../../../components/ui/button/Button.tsx";
import {
Expand Down
39 changes: 34 additions & 5 deletions client/src/pages/teachersPage/TeachersPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,33 @@ import { CardsList } from "../../components/cardsList/CardsList";
import { Pagination } from "../../components/ui/pagination/Pagination";
import { useTeachersQuery } from "../../features/teachers/query/useTeachersQuery.tsx";
import { useTeachersFiltersStore } from "../../store/filters.store.ts";
import { useEffect, useMemo } from "react";
import { useEffect, useMemo, useRef } from "react";
import { TeachersQuery } from "../../api/teacher/teacher.type.ts";
import { useShallow } from "zustand/react/shallow";
import { useSearchParams } from "react-router-dom";
import { TeachersCardsSkeletonList } from "../../components/skeletons/TeachersCardsSkeletonList.tsx";
import { useSubjectsQuery } from "../../features/subjects/query/useSubjectsQuery.tsx";
import { mapSubjectsToOptions } from "../../util/mapSubjectToOptions.util.ts";
import { FiltersSkeleton } from "../../components/skeletons/FiltersSkeleton.tsx";

export const TeachersPage = () => {
const [sp] = useSearchParams();
const setFromExternalQuery = useTeachersFiltersStore(
(s) => s.setFromExternalQuery,
);

const listTopRef = useRef<HTMLDivElement | null>(null);

const handlePageChange = (page: number) => {
setPage(page);

requestAnimationFrame(() => {
listTopRef.current?.scrollIntoView({
block: "start",
});
});
};

const {
subject,
minPrice,
Expand Down Expand Up @@ -62,12 +78,15 @@ export const TeachersPage = () => {
pageSize,
]);
const { data, isFetching } = useTeachersQuery(params);
const { data: radioGroupSubjects, isLoading: radioGroupSubjectsLoading } =
useSubjectsQuery();
useEffect(() => {
setFromExternalQuery({ subject: sp.get("subject") || undefined });
}, [sp, setFromExternalQuery]);
const showSkeleton = isFetching;

return (
<div
ref={listTopRef}
className="
flex flex-col items-center justify-center
w-full mx-auto
Expand All @@ -85,10 +104,20 @@ export const TeachersPage = () => {
</h3>
<div className="flex flex-col gap-10 lg:flex-row lg:items-start w-full">
<div className="flex justify-center w-full lg:w-75 shrink-0">
<Filters />
{radioGroupSubjectsLoading ? (
<FiltersSkeleton />
) : (
<Filters
radioGroupValues={
radioGroupSubjects
? mapSubjectsToOptions(radioGroupSubjects)
: []
}
/>
)}
</div>
<div className="w-full lg:flex-1 min-w-0">
{showSkeleton ? (
{isFetching ? (
<TeachersCardsSkeletonList count={10} />
) : data?.items?.length ? (
<CardsList cards={data.items} />
Expand All @@ -101,7 +130,7 @@ export const TeachersPage = () => {
theme="primary"
shape="round"
activeIndex={pageNumber}
onIndexChange={setPage}
onIndexChange={handlePageChange}
totalPages={data?.pagesCount ?? 1}
/>
</div>
Expand Down
8 changes: 0 additions & 8 deletions client/src/util/createTestIdFilePath.js

This file was deleted.

File renamed without changes.
13 changes: 13 additions & 0 deletions client/src/util/mapSubjectToOptions.util.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { SubjectsType } from "../api/subjects/subjects.type.ts";

export type Option = {
label: string;
value: string;
};

export const mapSubjectsToOptions = (subjects: SubjectsType[]): Option[] => {
return subjects.map(({ id, name }) => ({
label: name,
value: id,
}));
};
2 changes: 2 additions & 0 deletions server/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { chatRouter } from "./routes/chatRoute.js";
import { streamRouter } from "./routes/streamRoute.js";

import { videoCallRouter } from "./routes/videoCallRoute.js";
import { subjectRouter } from "./routes/subjectRoute.js";

// Create an express server
const app = express();
Expand All @@ -23,6 +24,7 @@ app.use(express.json());
* As we also host our client code on heroku we want to separate the API endpoints.
*/
app.use("/api/auth", authRouter);
app.use("/api/subjects", subjectRouter);
app.use("/api/appointments", appointmentRouter);
app.use("/api/teachers", teacherRouter);
app.use("/api/reviews", reviewRouter);
Expand Down
3 changes: 3 additions & 0 deletions server/src/composition/composition.types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,7 @@ export const TYPES = {
VideoCallQuery: Symbol.for("VideoCallQuery"),
VideoCallService: Symbol.for("VideoCallService"),
VideoCallController: Symbol.for("VideoCallController"),
//subjects
SubjectsController: Symbol.for("SubjectsController"),
SubjectsQuery: Symbol.for("SubjectsQuery"),
};
5 changes: 5 additions & 0 deletions server/src/composition/compositionRoot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { VideoCallCommand } from "../repositories/commandRepositories/videoCall.
import { VideoCallController } from "../controllers/videoCall.controller.js";
import { VideoCallQuery } from "../repositories/queryRepositories/videoCall.query.js";
import { VideoCallService } from "../services/video/videoCall.service.js";
import { SubjectsController } from "../controllers/subjects.controller.js";
import { SubjectsQuery } from "../repositories/queryRepositories/subjects.query.js";

export const container = new Container();

Expand Down Expand Up @@ -90,6 +92,9 @@ container.bind(TYPES.ChatQuery).to(ChatQuery);
container.bind(TYPES.ChatCommand).to(ChatCommand);
container.bind(TYPES.ChatController).to(ChatController);
container.bind(TYPES.ConversationCommand).to(ConversationCommand);
//subjects
container.bind(TYPES.SubjectsController).to(SubjectsController);
container.bind(TYPES.SubjectsQuery).to(SubjectsQuery);

//video call
container.bind<StreamController>(TYPES.StreamController).to(StreamController);
Expand Down
21 changes: 21 additions & 0 deletions server/src/controllers/subjects.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TYPES } from "../composition/composition.types.js";
import { inject, injectable } from "inversify";
import { NextFunction, Response, Request } from "express";
import { SubjectsQuery } from "../repositories/queryRepositories/subjects.query.js";

@injectable()
export class SubjectsController {
constructor(
@inject(TYPES.SubjectsQuery) private subjectsQuery: SubjectsQuery,
) {}

async getSubjects(req: Request, res: Response, next: NextFunction) {
try {
const subjects = await this.subjectsQuery.getAllSubjects();

return res.status(200).json(subjects);
} catch (err) {
return next(err);
}
}
}
15 changes: 15 additions & 0 deletions server/src/db/schemes/subjects.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import mongoose from "mongoose";
import { WithId } from "mongodb";
import { SubjectsTypeDB } from "./types/subjects.types.js";

const SubjectsSchema = new mongoose.Schema<SubjectsTypeDB>(
{
id: { type: String, required: true, unique: true, index: true },
name: { type: String, required: true, index: true },
},
{ versionKey: false },
);
export const SubjectsModel = mongoose.model<WithId<SubjectsTypeDB>>(
"subjects",
SubjectsSchema,
);
4 changes: 4 additions & 0 deletions server/src/db/schemes/types/subjects.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SubjectsTypeDB = {
id: string;
name: string;
};
17 changes: 17 additions & 0 deletions server/src/repositories/queryRepositories/subjects.query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { injectable } from "inversify";
import { SubjectsModel } from "../../db/schemes/subjects.schema.js";
import { subjectsMapper } from "../../utils/mappers/subject.mapper.js";

@injectable()
export class SubjectsQuery {
async getAllSubjects(): Promise<ReturnType<typeof subjectsMapper>[]> {
try {
const subjects = await SubjectsModel.find().lean();
return subjects.map(subjectsMapper);
} catch (err: unknown) {
throw new Error("Something went wrong with getting all subjects", {
cause: err,
});
}
}
}
11 changes: 11 additions & 0 deletions server/src/routes/subjectRoute.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Router } from "express";
import { container } from "../composition/compositionRoot.js";
import { SubjectsController } from "../controllers/subjects.controller.js";
import { TYPES } from "../composition/composition.types.js";

export const subjectRouter = Router();
const subjectsController = container.get<SubjectsController>(
TYPES.SubjectsController,
);

subjectRouter.get("/", subjectsController.getSubjects.bind(subjectsController));
4 changes: 4 additions & 0 deletions server/src/types/subjects/subjects.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export type SubjectsViewType = {
id: string;
name: string;
};
Loading
Loading