diff --git a/src/assets/locals/ar.json b/src/assets/locals/ar.json index 6ca4ada..24fe0d8 100644 --- a/src/assets/locals/ar.json +++ b/src/assets/locals/ar.json @@ -302,7 +302,14 @@ "removeSplitBill": "إزالة التقسيم", "cardBalance": "رصيد البطاقة", "collectAtCounter": "استلام في المكتب", - "collectAtParking": "استلام في الموقف" + "collectAtParking": "استلام في الموقف", + "scheduled": "مجدول", + "pickupNow": "استلام الآن", + "pickupEstimate": "تقدير الاستلام", + "today": "اليوم", + "change": "تغيير", + "pickup":"استلام", + "setPickupTime":"تحديد وقت الاستلام" }, "address": { "title": "العنوان", diff --git a/src/assets/locals/en.json b/src/assets/locals/en.json index 6d5afeb..4928bad 100644 --- a/src/assets/locals/en.json +++ b/src/assets/locals/en.json @@ -314,7 +314,15 @@ "removeSplitBill": "Remove Split Bill", "cardBalance": "Card Balance", "collectAtCounter": "Collect at counter", - "collectAtParking": "Collect at parking" + "collectAtParking": "Collect at parking", + "scheduled": "Scheduled", + "pickupNow": "Pickup Now", + "pickupEstimate": "Pickup Estimate", + "now": "Now", + "today": "Today", + "change": "Change", + "pickup": "Pickup", + "setPickupTime": "Set Pickup Time" }, "address": { "title": "Address", diff --git a/src/components/Icons/order/TimeIcon.tsx b/src/components/Icons/order/TimeIcon.tsx index 4b2d374..eb0ac90 100644 --- a/src/components/Icons/order/TimeIcon.tsx +++ b/src/components/Icons/order/TimeIcon.tsx @@ -1,9 +1,10 @@ interface TimeIconType { className?: string; onClick?: () => void; + color?: string; } -const TimeIcon = ({ className, onClick }: TimeIconType) => { +const TimeIcon = ({ className, onClick, color }: TimeIconType) => { return ( { diff --git a/src/pages/checkout/components/pickupEstimate/BottomSheet.tsx b/src/pages/checkout/components/pickupEstimate/BottomSheet.tsx new file mode 100644 index 0000000..f6e0280 --- /dev/null +++ b/src/pages/checkout/components/pickupEstimate/BottomSheet.tsx @@ -0,0 +1,31 @@ +import { ProBottomSheet } from "components/ProBottomSheet/ProBottomSheet.tsx"; +import PickupEstimateContent from "pages/checkout/components/pickupEstimate/Content.tsx"; +import { useTranslation } from "react-i18next"; + +interface PickupEstimateBottomSheetProps { + isOpen: boolean; + onClose: () => void; + onSave: (date: string, time: string) => void; +} + +export function PickupEstimateBottomSheet({ + isOpen, + onClose, + onSave, +}: PickupEstimateBottomSheetProps) { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/src/pages/checkout/components/pickupEstimate/Content.tsx b/src/pages/checkout/components/pickupEstimate/Content.tsx new file mode 100644 index 0000000..c3caf4b --- /dev/null +++ b/src/pages/checkout/components/pickupEstimate/Content.tsx @@ -0,0 +1,340 @@ +import { Button } from "antd"; +import dayjs from "dayjs"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; +import Picker from "components/WheelPicker"; +import { SERVER_DATE_FORMAT } from "utils/constants.ts"; + +interface PickupEstimateContentProps { + onSave: (date: string, time: string) => void; + onClose: () => void; +} + +export default function PickupEstimateContent({ + onSave, + onClose, +}: PickupEstimateContentProps) { + const { t } = useTranslation(); + + // Generate day options: Today, Tomorrow, then formatted dates (90 days total for scrolling) + const dayOptions = useMemo(() => { + const options = []; + const today = dayjs(); + + // Today + options.push({ + value: "today", + label: "Today", + date: today.format(SERVER_DATE_FORMAT), + }); + + // Tomorrow + options.push({ + value: "tomorrow", + label: "Tomorrow", + date: today.add(1, "day").format(SERVER_DATE_FORMAT), + }); + + // Next days with formatted dates (up to 90 days total for smooth scrolling) + for (let i = 2; i < 90; i++) { + const date = today.add(i, "day"); + const formatted = date.toDate().toLocaleDateString("en-US", { + weekday: "long", + month: "long", + day: "numeric", + }); + options.push({ + value: `day-${i}`, + label: formatted, + date: date.format(SERVER_DATE_FORMAT), + }); + } + + return options; + }, []); + + // Generate time options: Now, then time slots (5 items total) + const timeOptions = useMemo(() => { + const options = []; + const now = dayjs(); + + // Now option + options.push({ + value: "now", + label: "Now", + time: now.format("HH:mm"), + }); + + // Calculate next 15-minute interval (always the next one, even if we're on a boundary) + const currentMinute = now.minute(); + let nextMinute = Math.floor(currentMinute / 15) * 15 + 15; + let nextHour = now.hour(); + + if (nextMinute >= 60) { + nextMinute = 0; + nextHour += 1; + if (nextHour >= 24) { + nextHour = 0; + } + } + + // Generate next 4 time slots (15-minute intervals) + let hour = nextHour; + let minute = nextMinute; + + for (let i = 1; i < 48; i++) { + const time = dayjs().hour(hour).minute(minute).second(0); + const formatted = time.format("h:mm A"); + + options.push({ + value: `time-${i}`, + label: formatted, + time: time.format("HH:mm"), + }); + + // Calculate next interval + minute += 15; + if (minute >= 60) { + minute = 0; + hour += 1; + if (hour >= 24) { + hour = 0; + } + } + } + + return options; + }, []); + + const [selectedDay, setSelectedDay] = useState("today"); + const [selectedTime, setSelectedTime] = useState("now"); + + const handlePickerChange = (value: { day?: string; time?: string }) => { + console.log(value); + if (value.day !== undefined) { + setSelectedDay(value.day); + } + if (value.time !== undefined) { + setSelectedTime(value.time); + } + }; + + const handleSave = () => { + const selectedDayOption = dayOptions.find( + (opt) => opt.value === selectedDay, + ); + const selectedTimeOption = timeOptions.find( + (opt) => opt.value === selectedTime, + ); + + if (selectedDayOption && selectedTimeOption) { + onSave(selectedDayOption.date, selectedTimeOption.time); + onClose(); + } + }; + + const handleTouchStart = (e: React.TouchEvent) => { + e.stopPropagation(); + }; + + const handleTouchMove = (e: React.TouchEvent) => { + e.stopPropagation(); + }; + + const handleTouchEnd = (e: React.TouchEvent) => { + e.stopPropagation(); + }; + + const handleMouseDown = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const handleMouseMove = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const handleMouseUp = (e: React.MouseEvent) => { + e.stopPropagation(); + }; + + const handleWheel = (e: React.WheelEvent) => { + e.stopPropagation(); + }; + + return ( + <> +
+ {/* Day and Time Pickers - Same Row */} +
+ {/* Day Picker */} +
+ handlePickerChange(value)} + width="100%" + height={250} + style={{ + position: "relative", + display: "flex", + backgroundColor: "var(--secondary-background)", + overflow: "hidden", + width: "100%", + height: 250, + border: "none", + borderRadius: "8px", + }} + aria-selected={false} + > + + {dayOptions.map((option) => ( + + {({ selected }) => ( +
+ {option.label} +
+ )} +
+ ))} +
+
+
+ + {/* Time Picker */} +
+ handlePickerChange(value)} + width="100%" + height={250} + style={{ + position: "relative", + display: "flex", + backgroundColor: "var(--secondary-background)", + overflow: "hidden", + width: "100%", + height: 250, + border: "none", + borderRadius: "8px", + }} + aria-selected={false} + > + + {timeOptions.map((option) => ( + + {({ selected }) => ( +
+ {option.label} +
+ )} +
+ ))} +
+
+
+
+
+ + {/* Save Button */} +
+ + +
+ + ); +} diff --git a/src/pages/checkout/components/pickupEstimate/Dialog.tsx b/src/pages/checkout/components/pickupEstimate/Dialog.tsx new file mode 100644 index 0000000..1998b9f --- /dev/null +++ b/src/pages/checkout/components/pickupEstimate/Dialog.tsx @@ -0,0 +1,27 @@ +import { Modal } from "antd"; +import { useTranslation } from "react-i18next"; + +import PickupEstimateContent from "pages/checkout/components/pickupEstimate/Content.tsx"; + +interface PickupEstimateDialogProps { + isOpen: boolean; + onClose: () => void; + onSave: (date: string, time: string) => void; +} + +export function PickupEstimateDialog({ isOpen, onClose, onSave }: PickupEstimateDialogProps) { + const { t } = useTranslation(); + + return ( + + + + ); +} diff --git a/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.module.css b/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.module.css new file mode 100644 index 0000000..c1b970a --- /dev/null +++ b/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.module.css @@ -0,0 +1,27 @@ +.timeButton { + height: 32px !important; + width: 100% !important; + border-radius: 888px !important; + font-weight: 600 !important; + font-size: 16 !important; + border: none; + background-color: rgba(95, 108, 123, 0.05); + color: rgba(95, 108, 123, 1); + box-shadow: none; +} + +.toggleButton { + height: 32px !important; + border-radius: 888px !important; + font-weight: 600 !important; + font-size: 16 !important; + border: none; + background-color: rgba(95, 108, 123, 0.05); + color: rgba(95, 108, 123, 1); + box-shadow: none; +} + +.active { + background-color: rgba(255, 183, 0, 0.12) !important; + color: rgba(204, 147, 0, 1) !important; +} diff --git a/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.tsx b/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.tsx new file mode 100644 index 0000000..5799fed --- /dev/null +++ b/src/pages/checkout/components/pickupEstimate/TimeEstimateCard.tsx @@ -0,0 +1,106 @@ +import useFormInstance from "antd/es/form/hooks/useFormInstance"; +import TimeIcon from "components/Icons/order/TimeIcon"; +import ProInputCard from "components/ProInputCard/ProInputCard.tsx"; +import ProText from "components/ProText"; +import { + selectCart, + updatePickupDate, + updatePickupTime, +} from "features/order/orderSlice"; +import useBreakPoint from "hooks/useBreakPoint.ts"; +import { useState, useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { useAppDispatch, useAppSelector } from "redux/hooks"; +import { PickupEstimateDialog } from "./Dialog"; +import { PickupEstimateBottomSheet } from "./BottomSheet"; + +export default function PickupTimeCard() { + const dispatch = useAppDispatch(); + const { t } = useTranslation(); + const form = useFormInstance(); + const { isDesktop } = useBreakPoint(); + const { pickupTime, pickupDate } = useAppSelector(selectCart); + const [isEstimateTimeOpen, setIsEstimateTimeOpen] = useState(false); + + const handleEstimateTimeSave = (date: string, time: string) => { + form.setFieldsValue({ pickupTime: time, pickupDate: date }); + dispatch(updatePickupTime(time)); + dispatch(updatePickupDate(date)); + }; + + const handleEstimateTimeClose = () => { + setIsEstimateTimeOpen(false); + }; + + useEffect(() => { + form.setFieldsValue({ pickupTime, pickupDate }); + }, [pickupTime, pickupDate]); + + return ( + <> + setIsEstimateTimeOpen(true)} + style={{ + fontWeight: 500, + fontStyle: "Medium", + fontSize: 14, + lineHeight: "140%", + letterSpacing: "0%", + color: "#FFB700", + cursor: "pointer", + }} + > + {t("checkout.change")} + + } + > + + + + + {pickupTime} + + + (Est. 10-15 minutes) + + + + {isDesktop ? ( + + ) : ( + + )} + + ); +} diff --git a/src/pages/checkout/page.tsx b/src/pages/checkout/page.tsx index 6bc88fe..db097c6 100644 --- a/src/pages/checkout/page.tsx +++ b/src/pages/checkout/page.tsx @@ -18,10 +18,10 @@ import CouponCard from "pages/cart/components/CouponCard"; import BriefMenuCard from "./components/BriefMenuCard"; import CustomerInformationCard from "./components/CustomerInformationCard"; import Ads1 from "components/Ads/Ads1"; -import TimeEstimateCard from "pages/cart/components/timeEstimate/TimeEstimateCard"; import { useEffect } from "react"; import { CarCard } from "./components/CarCard"; import { CollectWay } from "./components/CollectWay/CollectWay"; +import PickupTimeCard from "./components/pickupEstimate/TimeEstimateCard"; export default function CheckoutPage() { const { t } = useTranslation(); @@ -48,7 +48,7 @@ export default function CheckoutPage() { {orderType === OrderType.Pickup && } {(orderType === OrderType.Pickup || - orderType === OrderType.ScheduledOrder) && } + orderType === OrderType.ScheduledOrder) && } {orderType === OrderType.Pickup && } {orderType === OrderType.Gift && }