diff --git a/src/frontend/src/components/client/home page/ServiceListRow.tsx b/src/frontend/src/components/client/home page/ServiceListRow.tsx index 8b935c2a..5bb63cfb 100644 --- a/src/frontend/src/components/client/home page/ServiceListRow.tsx +++ b/src/frontend/src/components/client/home page/ServiceListRow.tsx @@ -222,7 +222,7 @@ const ServicesList: React.FC = ({ className = "" }) => { /> ) : ( -
+
{servicesWithData.map(({ service, serviceData }) => (
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 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 */} 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"); diff --git a/src/frontend/src/pages/client/home.tsx b/src/frontend/src/pages/client/home.tsx index 9034af10..a907d2c7 100644 --- a/src/frontend/src/pages/client/home.tsx +++ b/src/frontend/src/pages/client/home.tsx @@ -72,7 +72,7 @@ const ClientHomePage: React.FC = () => { )} {/* SECTION: Main content */} -
+
{/* SECTION: Header */} {/* SECTION: Categories */} @@ -87,7 +87,7 @@ const ClientHomePage: React.FC = () => {
{/* SECTION: Provider CTA */}
-
+

Not enough services in your area?

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"}
{/* Right: Provider Details */} -
+
{ /> )} -
+
{/* Use userProfile directly for SPHeaderNextjs */} {/** * Make the scrolling container explicit and pass it to the header diff --git a/src/frontend/src/pages/provider/notifications.tsx b/src/frontend/src/pages/provider/notifications.tsx index a7fe701f..20918a76 100644 --- a/src/frontend/src/pages/provider/notifications.tsx +++ b/src/frontend/src/pages/provider/notifications.tsx @@ -346,14 +346,14 @@ const NotificationsPageSP = () => { 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"}
{/* Right: Certifications & Service Images */} -
+
{ 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);