From cfb7d23ae479750bad11811bf14a0600b43e7ae8 Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 20:02:30 +0800 Subject: [PATCH 01/11] Adjust layout for provider details section to improve responsiveness --- src/frontend/src/pages/client/service/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/client/service/[id].tsx b/src/frontend/src/pages/client/service/[id].tsx index f5905f6d..151e05ec 100644 --- a/src/frontend/src/pages/client/service/[id].tsx +++ b/src/frontend/src/pages/client/service/[id].tsx @@ -680,7 +680,7 @@ const ClientServiceDetailsPage: React.FC = () => { {/* Right: Provider Details */} -
+
Date: Wed, 6 May 2026 20:02:44 +0800 Subject: [PATCH 02/11] Scroll to top on booking confirmation page load --- src/frontend/src/pages/client/booking/confirmation.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/frontend/src/pages/client/booking/confirmation.tsx b/src/frontend/src/pages/client/booking/confirmation.tsx index 2a22bb68..52eade58 100644 --- a/src/frontend/src/pages/client/booking/confirmation.tsx +++ b/src/frontend/src/pages/client/booking/confirmation.tsx @@ -70,6 +70,7 @@ const BookingConfirmationPage: React.FC = () => { const location = useLocation(); const bookingDetails: BookingDetails | null = location.state?.details || null; useEffect(() => { + window.scrollTo(0, 0); document.title = "Booking Confirmed - SRV Client"; }, []); useNoBackNavigation("/client/booking"); From ce3b31d4285f32d9e6f0f0788ee3dc0dd2633ca8 Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 20:39:24 +0800 Subject: [PATCH 03/11] Scroll to top on successful validation before proceeding to the next step --- src/frontend/src/pages/provider/services/add.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/frontend/src/pages/provider/services/add.tsx b/src/frontend/src/pages/provider/services/add.tsx index 182d8fce..d29bdd52 100644 --- a/src/frontend/src/pages/provider/services/add.tsx +++ b/src/frontend/src/pages/provider/services/add.tsx @@ -620,9 +620,10 @@ const AddServicePage: React.FC = () => { return; } - if (Object.keys(errors).length === 0) { - setCurrentStep((prev) => prev + 1); - setValidationErrors({}); +if (Object.keys(errors).length === 0) { + scrollToTop(); + setCurrentStep((prev) => prev + 1); + setValidationErrors({}); } else { setValidationErrors(errors); setScrollToErrorTrigger((prev) => prev + 1); From 25848972e6a6cbfc2341f554c764586d7314b8a9 Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 20:49:19 +0800 Subject: [PATCH 04/11] fix: make right panel sticky on tablet mode for service details page --- src/frontend/src/pages/provider/service-details/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/provider/service-details/[id].tsx b/src/frontend/src/pages/provider/service-details/[id].tsx index e7c15e40..e691e5ce 100644 --- a/src/frontend/src/pages/provider/service-details/[id].tsx +++ b/src/frontend/src/pages/provider/service-details/[id].tsx @@ -1458,7 +1458,7 @@ const ProviderServiceDetailPage: React.FC = () => {
{/* Right: Certifications & Service Images */} -
+
Date: Wed, 6 May 2026 20:50:48 +0800 Subject: [PATCH 05/11] feat: implement ResponsiveSelect component for improved dropdown functionality in ServiceDetails and ServiceLocation --- .../provider/add service/ServiceDetails.tsx | 51 ++--- .../provider/add service/ServiceLocation.tsx | 51 +++-- .../src/components/ui/ResponsiveDropdown.tsx | 193 ++++++++++++++++++ 3 files changed, 238 insertions(+), 57 deletions(-) create mode 100644 src/frontend/src/components/ui/ResponsiveDropdown.tsx diff --git a/src/frontend/src/components/provider/add service/ServiceDetails.tsx b/src/frontend/src/components/provider/add service/ServiceDetails.tsx index e9db7f22..315645d0 100644 --- a/src/frontend/src/components/provider/add service/ServiceDetails.tsx +++ b/src/frontend/src/components/provider/add service/ServiceDetails.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect } from "react"; import { TrashIcon, PlusCircleIcon } from "@heroicons/react/24/solid"; import { ServiceCategory } from "../../../services/serviceCanisterService"; +import { ResponsiveSelect } from "../../ui/ResponsiveDropdown"; // Validation errors interface interface ValidationErrors { @@ -145,11 +146,6 @@ const ServiceDetails: React.FC = ({ handleChange(e); }; - const handleCategoryChange = (e: React.ChangeEvent) => { - setHideCategoryError(true); - handleChange(e); - }; - // Check if selected category requires client proof images const requiresProofKeywords = [ "repair", @@ -267,35 +263,28 @@ const ServiceDetails: React.FC = ({ > Select Category - + onChange={(value) => { + const event = { + target: { + name: "categoryId", + value: value, + }, + } as React.ChangeEvent; + handleChange(event); + setHideCategoryError(true); + }} + options={categories + .filter((cat) => !cat.parentId) + .map((cat) => ({ value: cat.id, label: cat.name }))} + placeholder="Select a category" + error={!!(validationErrors.categoryId && !hideCategoryError)} + loading={loadingCategories} + required={true} + /> {categoryRequiresProof && (
= ({ const [manualCityOptions, setManualCityOptions] = useState([]); // Handle province dropdown change - const handleProvinceChange = (e: React.ChangeEvent) => { - const province = e.target.value; + const handleProvinceChange = (province: string) => { setManualProvince(province); setManualCity(""); setFormData((prev: any) => ({ @@ -92,8 +92,7 @@ const ServiceLocation: React.FC = ({ }; // Handle city dropdown change - const handleCityChange = (e: React.ChangeEvent) => { - const city = e.target.value; + const handleCityChange = (city: string) => { setManualCity(city); setFormData((prev: any) => ({ ...prev, @@ -414,38 +413,38 @@ const ServiceLocation: React.FC = ({ Province * - + options={ + phLocations && Array.isArray(phLocations.provinces) + ? phLocations.provinces.map((prov: any) => ({ + value: prov.name, + label: prov.name, + })) + : [] + } + placeholder="Select Province" + /> - + />
)} diff --git a/src/frontend/src/components/ui/ResponsiveDropdown.tsx b/src/frontend/src/components/ui/ResponsiveDropdown.tsx new file mode 100644 index 00000000..5dfb8452 --- /dev/null +++ b/src/frontend/src/components/ui/ResponsiveDropdown.tsx @@ -0,0 +1,193 @@ +import { useState, useRef, useEffect, Fragment, ReactNode } from "react"; +import { ChevronDownIcon } from "@heroicons/react/24/solid"; + +interface ResponsiveDropdownProps { + triggerRef: React.RefObject; + isOpen: boolean; + onClose: () => void; + children: ReactNode; + position?: "left" | "right"; + width?: string; +} + +export function ResponsiveDropdown({ + triggerRef, + isOpen, + onClose, + children, + position = "left", + width = "w-full", +}: ResponsiveDropdownProps) { + const dropdownRef = useRef(null); + const [flipUp, setFlipUp] = useState(false); + + useEffect(() => { + if (isOpen && triggerRef.current) { + const rect = triggerRef.current.getBoundingClientRect(); + const spaceBelow = window.innerHeight - rect.bottom; + const spaceAbove = rect.top; + const threshold = 250; + + if (spaceBelow < threshold && spaceAbove > spaceBelow) { + setFlipUp(true); + } else { + setFlipUp(false); + } + } + }, [isOpen, triggerRef]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) && + triggerRef.current && + !triggerRef.current.contains(event.target as Node) + ) { + onClose(); + } + }; + + if (isOpen) { + document.addEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [isOpen, onClose, triggerRef]); + + return ( + + {isOpen && ( +
+ {children} +
+ )} +
+ ); +} + +interface SelectOption { + value: string; + label: string; +} + +interface ResponsiveSelectProps { + name: string; + id: string; + value: string; + onChange: (value: string) => void; + options: SelectOption[]; + placeholder?: string; + error?: boolean; + disabled?: boolean; + loading?: boolean; + required?: boolean; +} + +export function ResponsiveSelect({ + name, + id, + value, + onChange, + options, + placeholder = "Select an option", + error = false, + disabled = false, + loading = false, + required = false, +}: ResponsiveSelectProps) { + const [isOpen, setIsOpen] = useState(false); + const triggerRef = useRef(null); + + const selectedOption = options.find((opt) => opt.value === value); + + const handleSelect = (optionValue: string) => { + onChange(optionValue); + setIsOpen(false); + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Escape") { + setIsOpen(false); + } + }; + + return ( +
+ + + setIsOpen(false)} + width="w-full" + position="left" + > + {options.length === 0 ? ( +
+ No options available +
+ ) : ( + options.map((option) => ( + + )) + )} +
+ + {required && ( + + )} +
+ ); +} \ No newline at end of file From 0323a205479131501123f1dc3fa188e98f6350db Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 20:55:37 +0800 Subject: [PATCH 06/11] fix: ensure sticky right panel works on tablet by adding min-height to main --- src/frontend/src/pages/provider/service-details/[id].tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/src/pages/provider/service-details/[id].tsx b/src/frontend/src/pages/provider/service-details/[id].tsx index e691e5ce..52b8e6a2 100644 --- a/src/frontend/src/pages/provider/service-details/[id].tsx +++ b/src/frontend/src/pages/provider/service-details/[id].tsx @@ -1355,7 +1355,7 @@ const ProviderServiceDetailPage: React.FC = () => { {/* Main Content */} -
+
{/** Compute edit-lock state */} {/** Only the section currently being edited remains interactive */} {/** Others get blurred and non-interactive to prevent concurrent edits */} From d56ef5aa870cb902a07e2c134abf3276721a015f Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 21:49:30 +0800 Subject: [PATCH 07/11] fix: reduce bottom padding on BookingPage for improved layout --- src/frontend/src/pages/client/book/[id].tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/src/pages/client/book/[id].tsx b/src/frontend/src/pages/client/book/[id].tsx index e7a04af5..9c345140 100644 --- a/src/frontend/src/pages/client/book/[id].tsx +++ b/src/frontend/src/pages/client/book/[id].tsx @@ -1585,7 +1585,7 @@ const BookingPage: React.FC = () => {
-
+
{/* Left Column Skeleton */} @@ -1787,7 +1787,7 @@ const BookingPage: React.FC = () => {
-
+
{/* Left column (Desktop): Packages → Schedule → Location */} From 523e548f8ca83c5bea5008c083e72485c9101c9c Mon Sep 17 00:00:00 2001 From: dounmte Date: Wed, 6 May 2026 23:11:26 +0800 Subject: [PATCH 08/11] fix: adjust button padding and font size for improved accessibility on notifications page --- src/frontend/src/pages/client/notifications.tsx | 6 +++--- src/frontend/src/pages/provider/notifications.tsx | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/frontend/src/pages/client/notifications.tsx b/src/frontend/src/pages/client/notifications.tsx index 7ec90829..5c741789 100644 --- a/src/frontend/src/pages/client/notifications.tsx +++ b/src/frontend/src/pages/client/notifications.tsx @@ -315,14 +315,14 @@ const NotificationsPage = () => { clearSelection(); } }} - className="rounded-lg bg-gray-100 px-3 py-2 text-sm font-semibold text-gray-700 shadow-sm hover:bg-gray-50" + className="rounded-lg bg-gray-100 px-2 py-2 text-xs sm:text-sm sm:px-3 font-semibold text-gray-700 shadow-sm hover:bg-gray-50" > {editMode ? "Done" : "Edit"}