From 75f40df7e08e468eb538af4a8fee1e7ad217bf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Afra=20=E2=80=8C?= Date: Sat, 20 Jun 2026 12:17:44 +0330 Subject: [PATCH] feat: customer reservation flow --- package.json | 6 +- pnpm-lock.yaml | 98 ++++++++++ src/App.tsx | 24 ++- src/assets/call.svg | 3 + src/assets/info.svg | 3 + src/assets/location.svg | 4 + src/assets/minus.svg | 1 + src/assets/plus.svg | 3 + src/components/InfoIcon.tsx | 15 ++ src/components/ReservationModal.tsx | 211 +++++++++++++++++++++ src/data/mockCafes.ts | 55 ++++++ src/pages/CafeDetail.tsx | 272 ++++++++++++++++++++++++++++ src/pages/Home.tsx | 127 ++++++++++--- src/utils/formatNumber.ts | 3 + 14 files changed, 796 insertions(+), 29 deletions(-) create mode 100644 src/assets/call.svg create mode 100644 src/assets/info.svg create mode 100644 src/assets/location.svg create mode 100644 src/assets/minus.svg create mode 100644 src/assets/plus.svg create mode 100644 src/components/InfoIcon.tsx create mode 100644 src/components/ReservationModal.tsx create mode 100644 src/data/mockCafes.ts create mode 100644 src/pages/CafeDetail.tsx create mode 100644 src/utils/formatNumber.ts diff --git a/package.json b/package.json index d0ee15d..ee7f152 100644 --- a/package.json +++ b/package.json @@ -13,12 +13,16 @@ "dependencies": { "@tanstack/react-query": "^5.9.0", "axios": "^1.6.6", + "leaflet": "^1.9.4", "react": "^19.2.6", - "react-dom": "^19.2.6" + "react-dom": "^19.2.6", + "react-leaflet": "^5.0.0", + "react-router-dom": "^7.17.0" }, "devDependencies": { "@eslint/js": "^10.0.1", "@tailwindcss/vite": "^4.3.1", + "@types/leaflet": "^1.9.21", "@types/node": "^20.5.1", "@types/react": "^19.2.14", "@types/react-dom": "^19.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cebedaf..3f41338 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,12 +14,21 @@ importers: axios: specifier: ^1.6.6 version: 1.17.0 + leaflet: + specifier: ^1.9.4 + version: 1.9.4 react: specifier: ^19.2.6 version: 19.2.7 react-dom: specifier: ^19.2.6 version: 19.2.7(react@19.2.7) + react-leaflet: + specifier: ^5.0.0 + version: 5.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + react-router-dom: + specifier: ^7.17.0 + version: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) devDependencies: '@eslint/js': specifier: ^10.0.1 @@ -27,6 +36,9 @@ importers: '@tailwindcss/vite': specifier: ^4.3.1 version: 4.3.1(vite@8.0.16(@types/node@20.19.43)(jiti@2.7.0)) + '@types/leaflet': + specifier: ^1.9.21 + version: 1.9.21 '@types/node': specifier: ^20.5.1 version: 20.19.43 @@ -232,6 +244,13 @@ packages: '@oxc-project/types@0.133.0': resolution: {integrity: sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==} + '@react-leaflet/core@3.0.0': + resolution: {integrity: sha512-3EWmekh4Nz+pGcr+xjf0KNyYfC3U2JjnkWsh0zcqaexYqmmB5ZhH37kz41JXGmKzpaMZCnPofBBm64i+YrEvGQ==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + '@rolldown/binding-android-arm64@1.0.3': resolution: {integrity: sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -441,9 +460,15 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/json-schema@7.0.15': resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/leaflet@1.9.21': + resolution: {integrity: sha512-TbAd9DaPGSnzp6QvtYngntMZgcRk+igFELwR2N99XZn7RXUdKgsXMR+28bUO0rPsWp8MIu/f47luLIQuSLYv/w==} + '@types/node@20.19.43': resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==} @@ -589,6 +614,10 @@ packages: convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -862,6 +891,9 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + leaflet@1.9.4: + resolution: {integrity: sha512-nxS1ynzJOmOlHp+iL3FyWqK89GtNL8U8rvlMOsQdTTssxZwCXh8N2NB3GDQOL+YR3XnWyZAxwQixURb+FA74PA==} + levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} @@ -1032,6 +1064,30 @@ packages: peerDependencies: react: ^19.2.7 + react-leaflet@5.0.0: + resolution: {integrity: sha512-CWbTpr5vcHw5bt9i4zSlPEVQdTVcML390TjeDG0cK59z1ylexpqC6M1PJFjV8jD7CF+ACBFsLIDs6DRMoLEofw==} + peerDependencies: + leaflet: ^1.9.0 + react: ^19.0.0 + react-dom: ^19.0.0 + + react-router-dom@7.17.0: + resolution: {integrity: sha512-fyU2yjGups/hE6Xz0I5ZYbVL8Gx29eCjgpHaRaTaVU+OOAdfRX05KsvyRm0GO8YQwOkhpU3MurW1jyMUJn+zSw==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + + react-router@7.17.0: + resolution: {integrity: sha512-FDELK7rTMlCHO5+reyXsPlmfr7N1F91lPHsWYfMEGQm/KQ+F4JFM8jGoeQDmDvdTs93Fw9aSilH+uKRb4/jXvQ==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + react@19.2.7: resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} engines: {node: '>=0.10.0'} @@ -1053,6 +1109,9 @@ packages: engines: {node: '>=10'} hasBin: true + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1377,6 +1436,12 @@ snapshots: '@oxc-project/types@0.133.0': {} + '@react-leaflet/core@3.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + leaflet: 1.9.4 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + '@rolldown/binding-android-arm64@1.0.3': optional: true @@ -1512,8 +1577,14 @@ snapshots: '@types/estree@1.0.9': {} + '@types/geojson@7946.0.16': {} + '@types/json-schema@7.0.15': {} + '@types/leaflet@1.9.21': + dependencies: + '@types/geojson': 7946.0.16 + '@types/node@20.19.43': dependencies: undici-types: 6.21.0 @@ -1691,6 +1762,8 @@ snapshots: convert-source-map@2.0.0: {} + cookie@1.1.1: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1950,6 +2023,8 @@ snapshots: dependencies: json-buffer: 3.0.1 + leaflet@1.9.4: {} + levn@0.4.1: dependencies: prelude-ls: 1.2.1 @@ -2080,6 +2155,27 @@ snapshots: react: 19.2.7 scheduler: 0.27.0 + react-leaflet@5.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + '@react-leaflet/core': 3.0.0(leaflet@1.9.4)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + leaflet: 1.9.4 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + + react-router-dom@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + react-router: 7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + + react-router@7.17.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7): + dependencies: + cookie: 1.1.1 + react: 19.2.7 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.7(react@19.2.7) + react@19.2.7: {} rolldown@1.0.3: @@ -2109,6 +2205,8 @@ snapshots: semver@7.8.4: {} + set-cookie-parser@2.7.2: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/App.tsx b/src/App.tsx index ae18f15..c98b041 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,6 +1,26 @@ -import React from 'react' +import { useEffect } from 'react' +import { BrowserRouter, Routes, Route, useLocation } from 'react-router-dom' import Home from './pages/Home' +import CafeDetail from './pages/CafeDetail' + +function ScrollToTop() { + const { pathname } = useLocation() + + useEffect(() => { + window.scrollTo(0, 0) + }, [pathname]) + + return null +} export default function App() { - return + return ( + + + + } /> + } /> + + + ) } diff --git a/src/assets/call.svg b/src/assets/call.svg new file mode 100644 index 0000000..693c4bc --- /dev/null +++ b/src/assets/call.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/info.svg b/src/assets/info.svg new file mode 100644 index 0000000..1b66977 --- /dev/null +++ b/src/assets/info.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/assets/location.svg b/src/assets/location.svg new file mode 100644 index 0000000..462a973 --- /dev/null +++ b/src/assets/location.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/assets/minus.svg b/src/assets/minus.svg new file mode 100644 index 0000000..e1d50eb --- /dev/null +++ b/src/assets/minus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/plus.svg b/src/assets/plus.svg new file mode 100644 index 0000000..fc2de2c --- /dev/null +++ b/src/assets/plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/components/InfoIcon.tsx b/src/components/InfoIcon.tsx new file mode 100644 index 0000000..fa2bd6c --- /dev/null +++ b/src/components/InfoIcon.tsx @@ -0,0 +1,15 @@ +export default function InfoIcon({ className = 'h-5 w-5 shrink-0 text-gray-400' }: { className?: string }) { + return ( + + + + ) +} diff --git a/src/components/ReservationModal.tsx b/src/components/ReservationModal.tsx new file mode 100644 index 0000000..0994263 --- /dev/null +++ b/src/components/ReservationModal.tsx @@ -0,0 +1,211 @@ +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { Cafe } from "../data/mockCafes"; +import { formatPersianNumber } from "../utils/formatNumber"; +import InfoIcon from "./InfoIcon"; + +interface ReservationModalProps { + isOpen: boolean; + onClose: () => void; + cafe: Cafe; +} + +const mockPhone = "۰۹۱۲۳۴۵۶۷۸۹"; +// mock wallet balance — will come from the user's wallet once auth is implemented +const walletBalance = 10000; + +function MinusIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function PlusIcon({ className = "" }: { className?: string }) { + return ( + + + + ); +} + +function PaymentRow({ + label, + amount, + emphasize = false, + bottomBorder = false, + infoIconParam = false, +}: { + label: string; + amount: number; + emphasize?: boolean; + bottomBorder?: boolean; + infoIconParam?: boolean; +}) { + return ( +
+
+ + {label} + + {infoIconParam && ( + + )} +
+ + {formatPersianNumber(amount)} تومان + +
+ ); +} + +export default function ReservationModal({ + isOpen, + onClose, + cafe, +}: ReservationModalProps) { + const [fullName, setFullName] = useState(""); + const [count, setCount] = useState(1); + + useEffect(() => { + if (!isOpen) return; + + const previousOverflow = document.body.style.overflow; + document.body.style.overflow = "hidden"; + return () => { + document.body.style.overflow = previousOverflow; + }; + }, [isOpen]); + + if (!isOpen) return null; + + const reservationAmount = cafe.entryFeeAmount * count; + const payableAmount = Math.max(0, reservationAmount - walletBalance); + + return createPortal( + <> +
+
+
+ +
+

+ {cafe.name} +

+

+ {cafe.match} +

+ +
+ + setFullName(e.target.value)} + placeholder="نام و نام خانوادگی" + className="w-full rounded-2xl border border-gray-200 px-4 py-3 text-right" + /> +
+ +
+ + {/* readOnly mock for now — once OTP auth ships, this comes from the auth context */} + +
+ +
+ +
+ + + {formatPersianNumber(count)} نفر + + +
+
+ +

+ پرداخت +

+ + + + +
+ +

+ توجه داشته باشید که مسئولیت رزرو و حضور به‌موقع در کافه بر عهده + کاربر است. +

+
+
+ +
+ +
+
+ , + document.body, + ); +} diff --git a/src/data/mockCafes.ts b/src/data/mockCafes.ts new file mode 100644 index 0000000..6f720da --- /dev/null +++ b/src/data/mockCafes.ts @@ -0,0 +1,55 @@ +export interface Cafe { + id: number + name: string + match: string + entryFee: string + entryFeeAmount: number + remainingCapacity: number + tags: string[] + location: { lat: number; lng: number } + phone: string + address: string + images: string[] // empty array for now, will show gray placeholder +} + +export const mockCafes: Cafe[] = [ + { + id: 1, + name: 'کافه بی‌بای', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: '۲,۰۰۰,۰۰۰ تومان', + entryFeeAmount: 2000000, + remainingCapacity: 3, + tags: ['پارکینگ دارد', 'فضای سیگار', 'امکان سفارش'], + location: { lat: 35.7219, lng: 51.3347 }, + phone: '۰۲۱۹۱۰۲۰۲۱۲۰۲', + address: 'ونک، خدامی، نبش نیروی انتظامی، پلاک ۲', + images: [], + }, + { + id: 2, + name: 'کافه بی‌بای شعبه ۲', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: '۱,۵۰۰,۰۰۰ تومان', + entryFeeAmount: 1500000, + remainingCapacity: 10, + tags: ['فضای سیگار', 'امکان سفارش'], + location: { lat: 35.755, lng: 51.4165 }, + phone: '۰۲۱۸۸۴۴۵۵۶۶', + address: 'جردن، خیابان آفریقا، پلاک ۱۲', + images: [], + }, + { + id: 3, + name: 'کافه ستاره', + match: 'بازی برزیل-مراکش (پنجشنبه ۲۷ خرداد - ساعت ۲:۰۰)', + entryFee: 'رایگان', + entryFeeAmount: 0, + remainingCapacity: 15, + tags: ['پارکینگ دارد'], + location: { lat: 35.6892, lng: 51.389 }, + phone: '۰۲۱۷۷۳۳۲۲۱۱', + address: 'انقلاب، خیابان ۱۶ آذر، پلاک ۵', + images: [], + }, +] diff --git a/src/pages/CafeDetail.tsx b/src/pages/CafeDetail.tsx new file mode 100644 index 0000000..4a0b445 --- /dev/null +++ b/src/pages/CafeDetail.tsx @@ -0,0 +1,272 @@ +import { ReactNode, useState } from "react"; +import { useNavigate, useParams, useLocation } from "react-router-dom"; +import { MapContainer, TileLayer, Marker, ZoomControl } from "react-leaflet"; +import "leaflet/dist/leaflet.css"; +import L from "leaflet"; +import markerIcon2x from "leaflet/dist/images/marker-icon-2x.png"; +import markerIcon from "leaflet/dist/images/marker-icon.png"; +import markerShadow from "leaflet/dist/images/marker-shadow.png"; +import { mockCafes, Cafe } from "../data/mockCafes"; +import InfoIcon from "../components/InfoIcon"; +import ReservationModal from "../components/ReservationModal"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +delete (L.Icon.Default.prototype as any)._getIconUrl; +L.Icon.Default.mergeOptions({ + iconUrl: markerIcon, + iconRetinaUrl: markerIcon2x, + shadowUrl: markerShadow, +}); + +function ChevronLeftIcon() { + return ( + + + + ); +} + +function ChevronRightIcon() { + return ( + + + + ); +} + +function PhoneIcon() { + return ( + + + + ); +} + +function MapPinIcon() { + return ( + + + + + ); +} + +function Pill({ + children, + className = "bg-gray-100 text-gray-700", +}: { + children: ReactNode; + className?: string; +}) { + return ( + + {children} + + ); +} + +function ImageCarousel({ images }: { images: string[] }) { + const [index, setIndex] = useState(0); + const slideCount = Math.max(images.length, 1); + + return ( +
+ + +
+ {Array.from({ length: slideCount }).map((_, i) => ( + + ))} +
+
+ ); +} + +function toEnglishDigits(value: string) { + const persianDigits = "۰۱۲۳۴۵۶۷۸۹"; + return value.replace(/[۰-۹]/g, (digit) => String(persianDigits.indexOf(digit))); +} + +async function openNavigationPicker(cafe: Cafe) { + const mapsUrl = `https://www.google.com/maps/dir/?api=1&destination=${cafe.location.lat},${cafe.location.lng}`; + + if (navigator.share) { + try { + await navigator.share({ title: cafe.name, url: mapsUrl }); + } catch { + // user dismissed the native share sheet + } + } else { + window.open(mapsUrl, "_blank"); + } +} + +export default function CafeDetail() { + const { id } = useParams(); + const location = useLocation(); + const navigate = useNavigate(); + const [isReservationOpen, setIsReservationOpen] = useState(false); + + const cafe: Cafe | undefined = + (location.state as { cafe?: Cafe } | null)?.cafe ?? + mockCafes.find((c) => c.id === Number(id)); + + if (!cafe) { + return ( +
+ کافه پیدا نشد +
+ ); + } + + return ( +
+
+ + +
+ +
+

+ {cafe.name} +

+

{cafe.match}

+ +
+ ورودی: نفری {cafe.entryFee} + ظرفیت باقی مانده: {cafe.remainingCapacity}نفر + {/*
*/} + + {/*
*/} + {cafe.tags.map((tag) => ( + + {tag} + + ))} +
+ +

+ آدرس +

+ + + + + + +
+
+ +
+

شماره تماس

+ + {cafe.phone} + +
+
+
+ +
+

آدرس

+ {cafe.address} +
+
+ + +
+ +
+ +

+ توجه داشته باشید که مسئولیت رزرو و حضور به‌موقع در کافه بر عهده + کاربر است. +

+
+
+ +
+ +
+ + setIsReservationOpen(false)} + cafe={cafe} + /> +
+ ); +} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index 9f8ae19..376a05d 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,70 +1,143 @@ import { useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { mockCafes, Cafe } from '../data/mockCafes' -type Cafe = { - id: number - name: string - seats: number - image: string | null -} - -const mockCafes: Cafe[] = [ - { id: 1, name: 'کافه بی‌بای', seats: 8, image: null }, - { id: 2, name: 'کافه بی‌بای', seats: 10, image: null }, - { id: 3, name: 'کافه ستاره', seats: 15, image: null }, +const cityOptions = ['تهران', 'اصفهان', 'مشهد'] +const matchOptions = [ + 'برزیل-مراکش (۳ روز دیگر-۲:۰۰)', + 'آرژانتین-فرانسه (۲ روز دیگر-۱۸:۰۰)', + 'اسپانیا-آلمان (۱ روز دیگر-۲۰:۳۰)', ] -function ChevronDownIcon() { +function ChevronDownIcon({ className = '' }: { className?: string }) { return ( - + ) } +function MapIcon() { + return ( + + + + + + ) +} + +function ListIcon() { + return ( + + + + + + + + + ) +} + function MapPlaceholder() { return (
- نقشه + بنر
) } -function SelectorRow({ label, value }: { label: string; value: string }) { +function SelectorRow({ + label, + value, + options, + onChange, +}: { + label: string + value: string + options: string[] + onChange: (value: string) => void +}) { + const [open, setOpen] = useState(false) + return ( -
+

{label}

+ + {open && ( + <> +
setOpen(false)} /> +
+ {options.map((option) => ( + + ))} +
+ + )}
) } function CafeCard({ cafe }: { cafe: Cafe }) { + const navigate = useNavigate() + return ( -
+ ) } export default function Home() { const [view, setView] = useState<'list' | 'map'>('list') + const [city, setCity] = useState(cityOptions[0]) + const [match, setMatch] = useState(matchOptions[0]) return (
- - + +

کافه‌ها

@@ -73,18 +146,20 @@ export default function Home() { type="button" onClick={() => setView('map')} className={`flex items-center gap-1 rounded-full px-3 py-1.5 text-sm ${ - view === 'map' ? 'bg-[#1e2d4a] text-white' : 'text-gray-600' + view === 'map' ? 'bg-[#223D5D] text-white' : 'text-gray-600' }`} > + نقشه
@@ -104,7 +179,7 @@ export default function Home() { diff --git a/src/utils/formatNumber.ts b/src/utils/formatNumber.ts new file mode 100644 index 0000000..cdb04c2 --- /dev/null +++ b/src/utils/formatNumber.ts @@ -0,0 +1,3 @@ +export function formatPersianNumber(n: number): string { + return n.toLocaleString('fa-IR') +}