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
21 changes: 0 additions & 21 deletions client/src/api/appointments/regularStudents.api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,3 @@ export const getRegularTeachersApi = async (
);
return response.data;
};

export const getAllRegularStudentsSlotsApi = async (): Promise<
WeeklyScheduleSlot[]
> => {
const response = await api.get(
`/api/appointments/regular/students?limit=1000`,
);
const appointments: Appointment[] = response.data.appointments;

const allSlots: WeeklyScheduleSlot[] = [];
appointments.forEach((appointment) => {
if (
appointment.weeklySchedule &&
Array.isArray(appointment.weeklySchedule)
) {
allSlots.push(...appointment.weeklySchedule);
}
});

return allSlots;
};
15 changes: 12 additions & 3 deletions client/src/components/appointmentCard/AppointmentCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,22 @@ import {
isInternalVideoCallLink,
} from "./appointmentCard.utils";

type AppointmentCardProps = {
export type AppointmentCardProps = {
appointment: Appointment;
teacherAvatar?: string | null;
isPast?: boolean;
onDelete?: () => void;
isRegularTeacher?: boolean;
onShowSchedule?: () => void;
};

export const AppointmentCard = ({
appointment,
teacherAvatar,
isPast = false,
onDelete,
isRegularTeacher = false,
onShowSchedule,
}: AppointmentCardProps) => {
const statusStyles = getStatusStyles(appointment.status);
const isInternalLink = isInternalVideoCallLink(appointment.videoCall);
Expand All @@ -29,15 +33,18 @@ export const AppointmentCard = ({
<div
className={`relative flex flex-col border rounded-[15px] md:rounded-[20px] lg:rounded-[25px] overflow-hidden ${
isPast ? "opacity-50" : ""
} ${statusStyles.borderCard}`}
} ${isRegularTeacher ? "border-purple-500" : statusStyles.borderCard}`}
style={{
backgroundColor: statusStyles.bgCard,
backgroundColor: isRegularTeacher ? "#1E1D28" : statusStyles.bgCard,
}}
>
<AppointmentStatusBar
date={appointment.date}
time={appointment.time}
statusStyles={statusStyles}
isRegularStudent={appointment.isRegularStudent}
isRegularTeacher={isRegularTeacher}
onShowSchedule={onShowSchedule}
/>

<div className="flex flex-col min-[480px]:flex-row items-center min-[480px]:items-center px-3 sm:px-4 md:px-6 py-3 sm:py-4 md:py-6 gap-3 sm:gap-4 md:gap-6">
Expand All @@ -50,6 +57,8 @@ export const AppointmentCard = ({
lesson={appointment.lesson}
teacherName={appointment.teacherName}
price={appointment.price}
weeklySchedule={appointment.weeklySchedule}
isRegularTeacher={isRegularTeacher}
/>

<AppointmentJoinButton
Expand Down
35 changes: 34 additions & 1 deletion client/src/components/appointmentCard/AppointmentInfo.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,40 @@
type AppointmentInfoProps = {
import { WeeklyScheduleSlot } from "../../types/appointments.types";

export type AppointmentInfoProps = {
lesson?: string;
teacherName?: string;
price: string;
weeklySchedule?: WeeklyScheduleSlot[];
isRegularTeacher?: boolean;
};

const formatSchedule = (weeklySchedule: WeeklyScheduleSlot[]) => {
const scheduleByDay: Record<string, number[]> = {};
weeklySchedule.forEach((slot) => {
if (!scheduleByDay[slot.day]) {
scheduleByDay[slot.day] = [];
}
scheduleByDay[slot.day].push(slot.hour);
});

Object.keys(scheduleByDay).forEach((day) => {
scheduleByDay[day].sort((a, b) => a - b);
});

return Object.entries(scheduleByDay)
.map(([day, hours]) => {
const hoursStr = hours.map((h) => `${h}:00`).join(", ");
return `${day}: ${hoursStr}`;
})
.join(" • ");
};

export const AppointmentInfo = ({
lesson,
teacherName,
price,
weeklySchedule,
isRegularTeacher = false,
}: AppointmentInfoProps) => {
return (
<div className="flex-1 flex flex-col gap-1 md:gap-2">
Expand All @@ -26,6 +53,12 @@ export const AppointmentInfo = ({
<div className="text-[14px] md:text-[16px] text-white">
{price} euro /hour
</div>

{isRegularTeacher && weeklySchedule && weeklySchedule.length > 0 && (
<div className="text-[11px] md:text-[13px] text-gray-400 mt-1 max-w-full">
{formatSchedule(weeklySchedule)}
</div>
)}
</div>
);
};
45 changes: 40 additions & 5 deletions client/src/components/appointmentCard/AppointmentStatusBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,57 @@ type AppointmentStatusBarProps = {
date: string;
time: string;
statusStyles: StatusStyles;
isRegularStudent?: boolean;
isRegularTeacher?: boolean;
onShowSchedule?: () => void;
};

export const AppointmentStatusBar = ({
date,
time,
statusStyles,
isRegularStudent = false,
isRegularTeacher = false,
onShowSchedule,
}: AppointmentStatusBarProps) => {
if (isRegularTeacher) {
return (
<div className="w-full px-4 md:px-6 py-2 md:py-3 flex items-center justify-between bg-purple-900/30">
<span className="text-[12px] md:text-[14px] font-medium text-purple-300">
Regular Teacher
</span>
{onShowSchedule && (
<button
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onShowSchedule();
}}
className="px-4 py-2 rounded-full text-sm font-medium transition-colors bg-white/20 hover:bg-white/30 text-white border border-white/30"
>
Show Schedule
</button>
)}
</div>
);
}

return (
<div
className={`w-full px-4 md:px-6 py-2 md:py-3 flex items-center justify-between ${statusStyles.bgStatus}`}
>
<span
className={`text-[12px] md:text-[14px] font-medium ${statusStyles.text}`}
>
{statusStyles.label}
</span>
<div className="flex items-center gap-2">
<span
className={`text-[12px] md:text-[14px] font-medium ${statusStyles.text}`}
>
{statusStyles.label}
</span>
{isRegularStudent && (
<span className="text-[10px] md:text-[12px] px-2 py-1 bg-purple-600 text-white rounded-full font-medium">
Regular
</span>
)}
</div>
<div className="text-right">
<div
className={`text-[12px] md:text-[14px] font-medium ${statusStyles.text}`}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useState } from "react";
import { useState, useMemo } from "react";
import { Button } from "../ui/button/Button";
import Cross from "../icons/Cross";
import { SelectComponent } from "../ui/select/Select";
Expand Down Expand Up @@ -28,21 +28,19 @@ const HOURS = Array.from({ length: 17 }, (_, i) => ({
label: `${i + 7}:00`,
}));

export const RegularStudentScheduleModal = ({
isOpen,
const ModalContent = ({
onClose,
onSave,
studentName,
initialSchedule = [],
occupiedSlots = [],
}: RegularStudentScheduleModalProps) => {
}: Omit<RegularStudentScheduleModalProps, "isOpen">) => {
const initialSavedSlots = useMemo(() => initialSchedule, [initialSchedule]);
const [savedSlots, setSavedSlots] =
useState<WeeklyScheduleSlot[]>(initialSchedule);
useState<WeeklyScheduleSlot[]>(initialSavedSlots);
const [currentDay, setCurrentDay] = useState<string>("Monday");
const [currentHour, setCurrentHour] = useState<number>(7);

if (!isOpen) return null;

const isDuplicate = savedSlots.some(
(slot) => slot.day === currentDay && slot.hour === currentHour,
);
Expand All @@ -59,8 +57,20 @@ export const RegularStudentScheduleModal = ({
const newSlot = { day: currentDay, hour: currentHour };
const updatedSlots = [...savedSlots, newSlot];
setSavedSlots(updatedSlots);
setCurrentDay("Monday");
setCurrentHour(7);

const nextHour = currentHour + 1;
if (nextHour <= 23) {
setCurrentHour(nextHour);
} else {
const currentDayIndex = DAYS.findIndex((d) => d.value === currentDay);
if (currentDayIndex < DAYS.length - 1) {
setCurrentDay(DAYS[currentDayIndex + 1].value);
setCurrentHour(7);
} else {
setCurrentDay("Monday");
setCurrentHour(7);
}
}
};

const handleRemoveSlot = (index: number) => {
Expand All @@ -73,9 +83,6 @@ export const RegularStudentScheduleModal = ({
};

const handleCancel = () => {
setSavedSlots(initialSchedule);
setCurrentDay("Monday");
setCurrentHour(7);
onClose();
};

Expand Down Expand Up @@ -204,3 +211,12 @@ export const RegularStudentScheduleModal = ({
</div>
);
};

export const RegularStudentScheduleModal = ({
isOpen,
...props
}: RegularStudentScheduleModalProps) => {
if (!isOpen) return null;

return <ModalContent {...props} />;
};
76 changes: 1 addition & 75 deletions client/src/components/sidebar/Sidebar.tsx
Original file line number Diff line number Diff line change
@@ -1,80 +1,6 @@
import React from "react";
import { Link, useLocation } from "react-router-dom";
import AppointmentsIcon from "../icons/Appointments";
import DashboardIcon from "../icons/Dashboard";
import Chat from "../icons/Chat";
import UsersIcon from "../icons/UsersIcon";
import {
chatRoutes,
studentBase,
studentPrivatesRoutesVariables,
teacherBase,
teacherPrivatesRoutesVariables,
} from "../../router/routesVariables/pathVariables.ts";
import { joinPath } from "../../util/joinPath.util.ts";
import { Logo } from "../logo/Logo.tsx";

export type MenuItem = {
name: string;
link: string;
icon: React.ElementType;
};

export const defaultStudentMenuItems: MenuItem[] = [
{
name: "Dashboard",
link: joinPath(studentBase, studentPrivatesRoutesVariables.dashboard),
icon: DashboardIcon,
},
{
name: "Appointments",
link: joinPath(studentBase, studentPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
// {
// name: "Video Call",
// link: joinPath(studentBase, studentPrivatesRoutesVariables.videoCall),
// icon: VideoCallIcon,
// },
{
name: "My Profile",
link: joinPath(studentBase, studentPrivatesRoutesVariables.profile),
icon: UsersIcon,
},
{
name: "Chat",
link: joinPath(studentBase, chatRoutes.root),
icon: Chat,
},
];

export const defaultTeacherMenuItems: MenuItem[] = [
{
name: "Dashboard",
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.dashboard),
icon: DashboardIcon,
},
{
name: "Appointments",
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
// {
// name: "Video Call",
// link: joinPath(teacherBase, teacherPrivatesRoutesVariables.videoCall),
// icon: VideoCallIcon,
// },
{
name: "My Profile",
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.profile),
icon: UsersIcon,
},
{
name: "Chat",
link: joinPath(teacherBase, chatRoutes.root),
icon: Chat,
},
];
import { type MenuItem, defaultStudentMenuItems } from "./sidebarMenuItems.ts";

type SidebarProps = {
items?: MenuItem[];
Expand Down
11 changes: 0 additions & 11 deletions client/src/components/sidebar/sidebarMenuItems.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import AppointmentsIcon from "../icons/Appointments";
import DashboardIcon from "../icons/Dashboard";
import Chat from "../icons/Chat";
import UsersIcon from "../icons/UsersIcon";
// import VideoCallIcon from "../icons/VideoCallIcon";
import {
chatRoutes,
studentBase,
Expand All @@ -30,11 +29,6 @@ export const defaultStudentMenuItems: MenuItem[] = [
link: joinPath(studentBase, studentPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
// {
// name: "Video Call",
// link: joinPath(studentBase, studentPrivatesRoutesVariables.videoCall),
// icon: VideoCallIcon,
// },
{
name: "My Profile",
link: joinPath(studentBase, studentPrivatesRoutesVariables.profile),
Expand All @@ -58,11 +52,6 @@ export const defaultTeacherMenuItems: MenuItem[] = [
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.appointments),
icon: AppointmentsIcon,
},
// {
// name: "Video Call",
// link: joinPath(teacherBase, teacherPrivatesRoutesVariables.videoCall),
// icon: VideoCallIcon,
// },
{
name: "My Profile",
link: joinPath(teacherBase, teacherPrivatesRoutesVariables.profile),
Expand Down
Loading
Loading