Skip to content
Merged
35 changes: 35 additions & 0 deletions client/src/components/icons/ChevronLeft.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
type Ref,
type SVGProps,
forwardRef,
memo,
type MemoExoticComponent,
type ForwardRefExoticComponent,
} from "react";

const ChevronLeftComponent = (
props: SVGProps<SVGSVGElement>,
ref: Ref<SVGSVGElement>,
) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
ref={ref}
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M15 19l-7-7 7-7"
/>
</svg>
);

export const ChevronLeft = memo(
forwardRef(ChevronLeftComponent),
) as MemoExoticComponent<ForwardRefExoticComponent<SVGProps<SVGSVGElement>>>;
35 changes: 35 additions & 0 deletions client/src/components/icons/ChevronRight.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
type Ref,
type SVGProps,
forwardRef,
memo,
type MemoExoticComponent,
type ForwardRefExoticComponent,
} from "react";

const ChevronRightComponent = (
props: SVGProps<SVGSVGElement>,
ref: Ref<SVGSVGElement>,
) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
ref={ref}
{...props}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M9 5l7 7-7 7"
/>
</svg>
);

export const ChevronRight = memo(
forwardRef(ChevronRightComponent),
) as MemoExoticComponent<ForwardRefExoticComponent<SVGProps<SVGSVGElement>>>;
51 changes: 4 additions & 47 deletions client/src/components/teacherProfileSection/LessonSchedule.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,11 @@ interface TimeSlot {
hour: number;
}

interface BookedSlot {
day: string;
hour: number;
studentName: string;
lesson: string;
}

interface LessonScheduleProps {
isOpen: boolean;
onClose: () => void;
onSave: (slots: TimeSlot[]) => Promise<void> | void;
initialSlots?: TimeSlot[];
bookedSlots?: BookedSlot[];
}

const DAYS = [
Expand All @@ -34,30 +26,20 @@ const DAYS = [

const HOURS = Array.from({ length: 17 }, (_, i) => i + 7);

const getFirstName = (fullName: string): string => {
return fullName.split(" ")[0] || fullName;
};

export const LessonSchedule = ({
isOpen,
onClose,
onSave,
initialSlots = [],
bookedSlots = [],
}: LessonScheduleProps) => {
const [selectedSlots, setSelectedSlots] = useState<Set<string>>(
() => new Set(initialSlots.map((slot) => `${slot.day}-${slot.hour}`)),
);

const bookedSlotsSet = new Set(
bookedSlots.map((slot) => `${slot.day}-${slot.hour}`),
);

if (!isOpen) return null;

const toggleSlot = (day: string, hour: number) => {
const key = `${day}-${hour}`;
if (bookedSlotsSet.has(key)) return;

setSelectedSlots((prev) => {
const newSet = new Set(prev);
Expand All @@ -70,7 +52,6 @@ export const LessonSchedule = ({
});
};

// Take selected times, save them, then close this popup.
const handleSave = async () => {
const slots: TimeSlot[] = Array.from(selectedSlots).map((key) => {
const [day, hour] = key.split("-");
Expand Down Expand Up @@ -126,37 +107,17 @@ export const LessonSchedule = ({
{HOURS.map((hour) => {
const key = `${day}-${hour}`;
const isSelected = selectedSlots.has(key);
const isBooked = bookedSlotsSet.has(key);
const bookedInfo = bookedSlots.find(
(slot) => `${slot.day}-${slot.hour}` === key,
);

return (
<td
key={hour}
onClick={() => toggleSlot(day, hour)}
className={`border border-gray-600 p-1 transition-colors min-w-[60px] min-h-[50px] ${
isBooked
? "bg-purple-800 cursor-not-allowed"
: isSelected
? "bg-purple-500 hover:bg-purple-600 cursor-pointer"
: "bg-gray-700 hover:bg-gray-600 cursor-pointer"
isSelected
? "bg-purple-500 hover:bg-purple-600 cursor-pointer"
: "bg-gray-700 hover:bg-gray-600 cursor-pointer"
}`}
>
{isBooked && bookedInfo && (
<div className="text-white text-[10px] text-center leading-tight">
<div className="font-semibold truncate">
{getFirstName(bookedInfo.studentName)}
</div>
<div className="truncate">
{bookedInfo.lesson}
</div>
<div className="text-[9px] opacity-75">
(Request)
</div>
</div>
)}
</td>
></td>
);
})}
</tr>
Expand All @@ -171,10 +132,6 @@ export const LessonSchedule = ({
<div className="w-6 h-6 bg-purple-500 rounded"></div>
<span className="text-white text-sm">free time lesson</span>
</div>
<div className="flex items-center gap-2">
<div className="w-6 h-6 bg-purple-800 rounded"></div>
<span className="text-white text-sm">booked lesson</span>
</div>
</div>

<div className="mt-6 flex justify-center">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import React from "react";
import { ChevronLeft } from "../../../icons/ChevronLeft";
import { ChevronRight } from "../../../icons/ChevronRight";

interface TimeProps {
onDateSelect: (date: Date) => void;
selectedDate: Date | null;
Expand All @@ -22,12 +26,15 @@ export function Calendar({ onDateSelect, selectedDate }: TimeProps) {
const weekDays = ["M", "T", "W", "Th", "F", "S", "Su"];

const currentDate = new Date();
const [viewYear, setViewYear] = React.useState(currentDate.getFullYear());
const [viewMonth, setViewMonth] = React.useState(currentDate.getMonth());

const today = currentDate.getDate();
const currentYear = currentDate.getFullYear();
const currentMonth = currentDate.getMonth();
const today = currentDate.getDate();

const handleDateClick = (day: number): void => {
const selectedDay = new Date(currentYear, currentMonth, day);
const selectedDay = new Date(viewYear, viewMonth, day);
const todayDate = new Date(currentYear, currentMonth, today);
todayDate.setHours(0, 0, 0, 0);
selectedDay.setHours(0, 0, 0, 0);
Expand All @@ -38,11 +45,44 @@ export function Calendar({ onDateSelect, selectedDate }: TimeProps) {
};

const isPastDate = (day: number): boolean => {
return day < today;
const dateToCheck = new Date(viewYear, viewMonth, day);
const todayDate = new Date(currentYear, currentMonth, today);
todayDate.setHours(0, 0, 0, 0);
dateToCheck.setHours(0, 0, 0, 0);
return dateToCheck < todayDate;
};

const totalDays = new Date(currentYear, currentMonth + 1, 0).getDate();
const startDay = new Date(currentYear, currentMonth, 1).getDay() || 7;
const isSelectedDate = (day: number): boolean => {
if (!selectedDate) return false;
return (
selectedDate.getDate() === day &&
selectedDate.getMonth() === viewMonth &&
selectedDate.getFullYear() === viewYear
);
};

const handlePreviousMonth = () => {
if (viewMonth === 0) {
setViewMonth(11);
setViewYear(viewYear - 1);
} else {
setViewMonth(viewMonth - 1);
}
};

const handleNextMonth = () => {
if (viewMonth === 11) {
setViewMonth(0);
setViewYear(viewYear + 1);
} else {
setViewMonth(viewMonth + 1);
}
};

const isCurrentMonth = viewYear === currentYear && viewMonth === currentMonth;

const totalDays = new Date(viewYear, viewMonth + 1, 0).getDate();
const startDay = new Date(viewYear, viewMonth, 1).getDay() || 7;

const calendarDays = [
...Array(startDay - 1).fill(null),
Expand All @@ -51,10 +91,27 @@ export function Calendar({ onDateSelect, selectedDate }: TimeProps) {

return (
<div className="bg-[#2C2436] rounded-xl p-4 sm:p-6 shadow-sm sm:mx-auto sm:max-w-fit">
<div className="flex justify-center items-center mb-4 sm:mb-6">
<div className="flex justify-between items-center mb-4 sm:mb-6">
<button
onClick={handlePreviousMonth}
disabled={isCurrentMonth}
className="text-white hover:text-[#7C86F7] disabled:text-gray-600 disabled:cursor-not-allowed transition-colors p-2"
aria-label="Previous month"
>
<ChevronLeft className="h-6 w-6" />
</button>

<h3 className="text-lg sm:text-xl font-semibold text-white">
{months[currentMonth]} {currentYear}
{months[viewMonth]} {viewYear}
</h3>

<button
onClick={handleNextMonth}
className="text-white hover:text-[#7C86F7] transition-colors p-2"
aria-label="Next month"
>
<ChevronRight className="h-6 w-6" />
</button>
</div>

<div className="grid grid-cols-7 gap-1 mb-3 sm:mb-4">
Expand All @@ -81,7 +138,7 @@ export function Calendar({ onDateSelect, selectedDate }: TimeProps) {
className={`w-full h-full rounded-[40px] flex items-center justify-center text-sm sm:text-base font-normal leading-6 transition-colors ${
isPastDate(day)
? "text-gray-600 cursor-not-allowed opacity-50"
: selectedDate && day === selectedDate.getDate()
: isSelectedDate(day)
? "bg-[#7C86F7] text-white"
: "text-white hover:bg-transparent hover:text-[#F3F2F5] sm:hover:bg-[#F3F2F5] sm:hover:text-black"
}`}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@ import {
import { useMyProfileQuery } from "../../../features/teachers/query/useMyProfileQuery";
import { useUpdateMyProfileMutation } from "../../../features/teachers/mutations/useUpdateMyProfileMutation";
import { useModalStore } from "../../../store/modals.store";
import { useTeacherAppointmentsQuery } from "../../../features/appointments/query/useTeacherAppointmentsQuery";
import { mapAppointmentsToBookedSlots } from "../../../util/appointmentSchedule.util";

export type { LessonPrice } from "../../../components/teacherProfileSection/types";

Expand All @@ -37,11 +35,6 @@ export const TeacherProfile = () => {
const { data: profile, isLoading } = useMyProfileQuery();
const updateProfileMutation = useUpdateMyProfileMutation();
const openModal = useModalStore((s) => s.open);
const { data: appointmentsData } = useTeacherAppointmentsQuery(
undefined,
1,
1000,
);

const [isEditing, setIsEditing] = useState(false);
const [name, setName] = useState("");
Expand Down Expand Up @@ -322,11 +315,6 @@ export const TeacherProfile = () => {
}
};

const getBookedSlots = () => {
const appointments = appointmentsData?.appointments || [];
return mapAppointmentsToBookedSlots(appointments);
};

if (isLoading) {
return (
<div className="min-h-screen bg-[#15141D]">
Expand Down Expand Up @@ -416,7 +404,6 @@ export const TeacherProfile = () => {
onClose={() => setIsScheduleOpen(false)}
onSave={handleScheduleSave}
initialSlots={schedule}
bookedSlots={getBookedSlots()}
/>

<ChangePasswordModal
Expand Down
Loading
Loading