pickup time: intial commit

This commit is contained in:
2026-01-06 22:41:50 +03:00
parent ad036d1e64
commit ebe9928091
9 changed files with 553 additions and 6 deletions

View File

@@ -302,7 +302,14 @@
"removeSplitBill": "إزالة التقسيم",
"cardBalance": "رصيد البطاقة",
"collectAtCounter": "استلام في المكتب",
"collectAtParking": "استلام في الموقف"
"collectAtParking": "استلام في الموقف",
"scheduled": "مجدول",
"pickupNow": "استلام الآن",
"pickupEstimate": "تقدير الاستلام",
"today": "اليوم",
"change": "تغيير",
"pickup":"استلام",
"setPickupTime":"تحديد وقت الاستلام"
},
"address": {
"title": "العنوان",

View File

@@ -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",

View File

@@ -1,9 +1,10 @@
interface TimeIconType {
className?: string;
onClick?: () => void;
color?: string;
}
const TimeIcon = ({ className, onClick }: TimeIconType) => {
const TimeIcon = ({ className, onClick, color }: TimeIconType) => {
return (
<svg
width="17"
@@ -17,7 +18,7 @@ const TimeIcon = ({ className, onClick }: TimeIconType) => {
<g clipPath="url(#clip0_2448_7814)">
<path
d="M8.50016 4.08171V8.08171L11.1668 9.41504M15.1668 8.08171C15.1668 11.7636 12.1821 14.7484 8.50016 14.7484C4.81826 14.7484 1.8335 11.7636 1.8335 8.08171C1.8335 4.39981 4.81826 1.41504 8.50016 1.41504C12.1821 1.41504 15.1668 4.39981 15.1668 8.08171Z"
stroke="#333333"
stroke={color || "#333333"}
strokeLinecap="round"
strokeLinejoin="round"
/>

View File

@@ -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 (
<ProBottomSheet
isOpen={isOpen}
onClose={onClose}
title={t("checkout.setPickupTime")}
showCloseButton={true}
initialSnap={1}
height={550}
snapPoints={[550]}
>
<PickupEstimateContent onSave={onSave} onClose={onClose} />
</ProBottomSheet>
);
}

View File

@@ -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<string>("today");
const [selectedTime, setSelectedTime] = useState<string>("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<HTMLDivElement>) => {
e.stopPropagation();
};
const handleTouchMove = (e: React.TouchEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handleTouchEnd = (e: React.TouchEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handleMouseUp = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
};
const handleWheel = (e: React.WheelEvent<HTMLDivElement>) => {
e.stopPropagation();
};
return (
<>
<div
style={{
marginTop: 30,
display: "flex",
flexDirection: "column",
}}
onTouchStart={handleTouchStart}
onTouchMove={handleTouchMove}
onTouchEnd={handleTouchEnd}
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
onWheel={handleWheel}
>
{/* Day and Time Pickers - Same Row */}
<div
style={{
display: "flex",
flexDirection: "row",
gap: "8px",
marginBottom: 30,
}}
>
{/* Day Picker */}
<div style={{ width: "60%" }}>
<Picker
value={{ day: selectedDay }}
onChange={(value) => 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}
>
<Picker.Column
name="day"
style={{
flex: 1,
backgroundColor: "var(--secondary-background)",
}}
>
{dayOptions.map((option) => (
<Picker.Item key={option.value} value={option.value}>
{({ selected }) => (
<div
style={{
fontWeight: selected ? "600" : "400",
color: selected
? "var(--foreground)"
: "var(--text-color-gray)",
fontSize: "16px",
padding: "9px 0",
textAlign: "center",
opacity: selected ? 1 : 0.7,
transition: "all 0.2s ease",
lineHeight: "1.2",
height: 36,
width: "100%",
backgroundColor: selected
? "var(--background)"
: "transparent",
borderRadius: selected ? "4px" : "0",
margin: selected ? "2px 4px" : "0",
}}
>
{option.label}
</div>
)}
</Picker.Item>
))}
</Picker.Column>
</Picker>
</div>
{/* Time Picker */}
<div style={{ width: "40%" }}>
<Picker
value={{ time: selectedTime }}
onChange={(value) => 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}
>
<Picker.Column
name="time"
style={{
flex: 1,
backgroundColor: "var(--secondary-background)",
}}
>
{timeOptions.map((option) => (
<Picker.Item key={option.value} value={option.value}>
{({ selected }) => (
<div
style={{
fontWeight: selected ? "600" : "400",
color: selected
? "var(--foreground)"
: "var(--text-color-gray)",
fontSize: "16px",
padding: "9px 0",
textAlign: "center",
opacity: selected ? 1 : 0.7,
transition: "all 0.2s ease",
lineHeight: "1.2",
height: 36,
width: "100%",
backgroundColor: selected
? "var(--background)"
: "transparent",
borderRadius: selected ? "4px" : "0",
margin: selected ? "2px 4px" : "0",
}}
>
{option.label}
</div>
)}
</Picker.Item>
))}
</Picker.Column>
</Picker>
</div>
</div>
</div>
{/* Save Button */}
<div style={{ display: "flex", flexDirection: "column", gap: 16 }}>
<Button
type="primary"
onClick={handleSave}
style={{
width: "100%",
height: 48,
fontSize: 16,
fontWeight: 600,
backgroundColor: "var(--primary)",
border: "none",
}}
>
{t("checkout.scheduled")}
</Button>
<Button
onClick={handleSave}
style={{
width: "100%",
height: 48,
fontSize: 16,
fontWeight: 600,
border: "1px solid ##C0BFC4",
}}
>
{t("checkout.pickupNow")}
</Button>
</div>
</>
);
}

View File

@@ -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 (
<Modal
title={t("checkout.pickupEstimate")}
open={isOpen}
onCancel={onClose}
footer={[]}
width={500}
destroyOnHidden
>
<PickupEstimateContent onSave={onSave} onClose={onClose} />
</Modal>
);
}

View File

@@ -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;
}

View File

@@ -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 (
<>
<ProInputCard
title={t("checkout.pickup")}
titleRight={
<ProText
onClick={() => setIsEstimateTimeOpen(true)}
style={{
fontWeight: 500,
fontStyle: "Medium",
fontSize: 14,
lineHeight: "140%",
letterSpacing: "0%",
color: "#FFB700",
cursor: "pointer",
}}
>
{t("checkout.change")}
</ProText>
}
>
<span style={{ position: "relative", top: 3 }}>
<TimeIcon color="#777580" />
</span>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 16,
lineHeight: "140%",
letterSpacing: "0%",
color: "#777580",
padding: "0 2px 0 4px",
}}
>
{pickupTime}
</ProText>
<ProText
style={{
fontWeight: 400,
fontStyle: "Regular",
fontSize: 16,
lineHeight: "140%",
letterSpacing: "0%",
color: "#777580",
padding: "0 2px",
}}
>
(Est. 10-15 minutes)
</ProText>
</ProInputCard>
{isDesktop ? (
<PickupEstimateDialog
isOpen={isEstimateTimeOpen}
onClose={handleEstimateTimeClose}
onSave={handleEstimateTimeSave}
/>
) : (
<PickupEstimateBottomSheet
isOpen={isEstimateTimeOpen}
onClose={handleEstimateTimeClose}
onSave={handleEstimateTimeSave}
/>
)}
</>
);
}

View File

@@ -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() {
<Layout.Content className={styles.checkoutContainer}>
{orderType === OrderType.Pickup && <CollectWay />}
{(orderType === OrderType.Pickup ||
orderType === OrderType.ScheduledOrder) && <TimeEstimateCard />}
orderType === OrderType.ScheduledOrder) && <PickupTimeCard />}
{orderType === OrderType.Pickup && <CarCard />}
{orderType === OrderType.Gift && <GiftCard />}