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
16 changes: 13 additions & 3 deletions client/src/components/table/LessonRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ import KebabVerticalIcon from "../icons/KebabVertical";
import UncheckedIcon from "../icons/Uncheked";
import Check from "../icons/Check";
import { StatusButtons } from "../ui/statusButtons/StatusButtons";
import { AppointmentStatus } from "../../types/appointments.types";

export type LessonRowData = {
id?: string | number;
checked: boolean; // checkbox state
checked: boolean;
linkText?: string;
[key: string]: ReactNode;
lesson?: string;
student?: string;
price?: string;
date?: string;
time?: string;
status?: AppointmentStatus;
onStatusChange?: (status: AppointmentStatus) => void;
} & {
[key: string]: ReactNode | string | number | boolean | undefined;
};

type LessonRowProps = {
Expand Down Expand Up @@ -74,9 +83,10 @@ const LessonRow = ({
| "approved"
| "rejected"
}
onStatusChange={data.onStatusChange}
/>
) : (
data[column.key]
(data[column.key] as ReactNode)
)}
</td>
))}
Expand Down
9 changes: 5 additions & 4 deletions client/src/components/table/LessonsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ const LessonsTable = ({
const [rows, setRows] = useState<LessonRowData[]>(sourceRows);

const handleToggleRow = (rowIndex: number) => {
setRows((prev) =>
prev.map((row, index) =>
index === rowIndex ? { ...row, checked: !row.checked } : row,
),
setRows(
(prev) =>
prev.map((row, index) =>
index === rowIndex ? { ...row, checked: !row.checked } : row,
) as LessonRowData[],
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,48 @@ import { useState } from "react";
import { Calendar } from "./Calendar/Calendar";
import { Time } from "./Time/Time";
import { Button } from "../../ui/button/Button";
import { Modal } from "../../ui/modal/Modal";
import { TeacherType } from "../../../types/teacher.types";

export default function TeacherSchedule() {
interface TeacherScheduleProps {
teacher?: TeacherType;
}

export default function TeacherSchedule({ teacher }: TeacherScheduleProps) {
const [selectedDate, setSelectedDate] = useState<Date | null>(null);
const [selectedTime, setSelectedTime] = useState<string | null>(null);
const [showTimeAndBook, setShowTimeAndBook] = useState<boolean>(false);
const [showConfirmModal, setShowConfirmModal] = useState<boolean>(false);

const handleDateSelection = (date: Date): void => {
setSelectedDate(date);
setShowTimeAndBook(true);
};

const handleTimeSelection = (time: string): void => {
setSelectedTime(time);
};

const handleBook = (): void => {
if (selectedDate && selectedTime && teacher) {
setShowConfirmModal(true);
}
};

const handleConfirmBooking = (): void => {
setShowConfirmModal(false);
setSelectedDate(null);
setSelectedTime(null);
setShowTimeAndBook(false);
};

const getAvailableTimeSlots = (): string[] => {
if (!teacher || !selectedDate) return [];

const dateStr = selectedDate.toISOString().split("T")[0];
return teacher.schedule?.[dateStr] || teacher.availableTimeSlots || [];
};

return (
<div>
<div className="bg-[#15141D] py-[40px] sm:py-[48px] px-[50px] relative rounded-3xl w-auto h-auto border border-[#7286FF]">
Expand All @@ -28,18 +60,53 @@ export default function TeacherSchedule() {

{showTimeAndBook && (
<div className="mt-8">
<Time onTimeSelect={() => {}} />
<Time
onTimeSelect={handleTimeSelection}
availableSlots={getAvailableTimeSlots()}
/>
</div>
)}

{showTimeAndBook && (
{showTimeAndBook && selectedTime && (
<div className="mt-8">
<Button variant="secondary">Book</Button>
<Button variant="secondary" onClick={handleBook}>
Book Lesson - ${teacher?.price || 0}
</Button>
</div>
)}
</div>
</div>
</div>

<Modal
isOpen={showConfirmModal}
onClose={() => setShowConfirmModal(false)}
title="Confirm Booking"
onConfirm={handleConfirmBooking}
confirmText="Book Now"
cancelText="Cancel"
>
<div className="space-y-2">
<p>
<strong>Teacher:</strong> {teacher?.name}
</p>
<p>
<strong>Subject:</strong> {teacher?.subject}
</p>
<p>
<strong>Date:</strong> {selectedDate?.toLocaleDateString()}
</p>
<p>
<strong>Time:</strong> {selectedTime}
</p>
<p>
<strong>Price:</strong> ${teacher?.price}
</p>
<p className="text-sm text-gray-600 mt-4">
The lesson request will be sent to the teacher.
</p>
</div>
</Modal>
</div>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { useState } from "react";

interface TimeProps {
onTimeSelect: (time: string) => void;
availableSlots?: string[];
}

export function Time({ onTimeSelect }: TimeProps) {
export function Time({ onTimeSelect, availableSlots }: TimeProps) {
const [selectedTime, setSelectedTime] = useState<string | null>(null);

const timeSlots = [
const defaultTimeSlots = [
"7:00",
"8:00",
"9:00",
Expand All @@ -25,6 +26,8 @@ export function Time({ onTimeSelect }: TimeProps) {
"21:00",
];

const timeSlots = availableSlots || defaultTimeSlots;

const handleTimeClick = (time: string): void => {
setSelectedTime(time);
onTimeSelect(time);
Expand Down
43 changes: 43 additions & 0 deletions client/src/components/ui/modal/Modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { ReactNode } from "react";
import { Button } from "../button/Button";

interface ModalProps {
isOpen: boolean;
onClose: () => void;
title: string;
children: ReactNode;
onConfirm?: () => void;
confirmText?: string;
cancelText?: string;
}

export const Modal = ({
isOpen,
onClose,
title,
children,
onConfirm,
confirmText = "Confirm",
cancelText = "Cancel",
}: ModalProps) => {
if (!isOpen) return null;

return (
<div className="fixed inset-0 z-50 flex items-center justify-center">
<div className="fixed inset-0 bg-black/50" onClick={onClose} />

<div className="relative bg-white rounded-lg p-6 max-w-md w-full mx-4 shadow-xl z-10">
<h2 className="text-lg font-semibold text-gray-900 mb-4">{title}</h2>

<div className="mb-6 text-gray-700">{children}</div>

<div className="flex gap-3 justify-end">
<Button variant="secondary" onClick={onClose}>
{cancelText}
</Button>
{onConfirm && <Button onClick={onConfirm}>{confirmText}</Button>}
</div>
</div>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "../../queryKeys";
import { Appointment } from "../../../types/appointments.types";

interface CreateAppointmentRequest {
teacherId: string;
studentId: string;
date: string;
time: string;
lesson: string;
price: string;
}

const createAppointment = async (
data: CreateAppointmentRequest,
): Promise<Appointment> => {
await new Promise((resolve) => setTimeout(resolve, 1000));

return {
id: Date.now().toString(),
lesson: data.lesson,
teacher: data.teacherId,
student: data.studentId,
price: data.price,
date: data.date,
time: data.time,
status: "pending",
videoCall: `https://meet.google.com/${data.teacherId}-${data.studentId}-${Date.now()}`,
};
};

export const useCreateAppointmentMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: createAppointment,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.appointments,
});
},
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { queryKeys } from "../../queryKeys";
import {
Appointment,
AppointmentStatus,
} from "../../../types/appointments.types";

interface UpdateAppointmentRequest {
appointmentId: string;
status: AppointmentStatus;
}

const updateAppointmentStatus = async (
data: UpdateAppointmentRequest,
): Promise<Appointment> => {
await new Promise((resolve) => setTimeout(resolve, 500));

return {
id: data.appointmentId,
lesson: "English",
teacher: "1",
student: "Anna Tkachuk",
price: "25 euro",
date: "5/27/15",
time: "2:00 PM",
status: data.status,
videoCall: `https://meet.google.com/1-anna-${data.appointmentId}`,
};
};

export const useUpdateAppointmentMutation = () => {
const queryClient = useQueryClient();

return useMutation({
mutationFn: updateAppointmentStatus,
onSuccess: () => {
queryClient.invalidateQueries({
queryKey: queryKeys.appointments,
});
},
onError: (error) => {
console.error("Failed to update appointment status:", error);
},
});
};
Loading
Loading