Merge pull request 'feat: map feature added' (#3) from feature/WNPT-366 into master
Reviewed-on: #3
This commit is contained in:
commit
ca7ccaa930
5 changed files with 162 additions and 51 deletions
|
|
@ -11,6 +11,7 @@ import AddEvent from './pages/AddEvent'
|
|||
import AddEventSuccess from './pages/AddEventSuccess'
|
||||
import AddEventFail from './pages/AddEventFail'
|
||||
import EventReservations from './pages/EventReservations'
|
||||
import CafeMapPage from './pages/CafeMapPage'
|
||||
import MyReservations from './pages/MyReservations'
|
||||
import Login from './pages/Login'
|
||||
import LoginOtp from './pages/LoginOtp'
|
||||
|
|
@ -35,6 +36,7 @@ export default function App() {
|
|||
<Route path="/" element={<Navigate to="/login" replace />} />
|
||||
<Route path="/home" element={<RequireRole role="customer"><Home /></RequireRole>} />
|
||||
<Route path="/cafe/:id" element={<RequireRole role="customer"><CafeDetail /></RequireRole>} />
|
||||
<Route path="/map" element={<RequireRole role="customer"><CafeMapPage /></RequireRole>} />
|
||||
<Route path="/payment/success" element={<RequireRole role="customer"><PaymentSuccess /></RequireRole>} />
|
||||
<Route path="/reservations" element={<RequireRole role="customer"><MyReservations /></RequireRole>} />
|
||||
<Route path="/owner/register" element={<RequireRole role="cafe_owner"><CafeOwnerRegister /></RequireRole>} />
|
||||
|
|
|
|||
72
src/pages/CafeMapPage.tsx
Normal file
72
src/pages/CafeMapPage.tsx
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { MapContainer, TileLayer, Marker, ZoomControl, useMap } from 'react-leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import '../utils/leafletIcons'
|
||||
import { Cafe } from '../services/campaignApi'
|
||||
import Icon from '../components/Icon'
|
||||
|
||||
function FitBounds({ cafes }: { cafes: Cafe[] }) {
|
||||
const map = useMap()
|
||||
useEffect(() => {
|
||||
if (cafes.length === 0) return
|
||||
if (cafes.length === 1) {
|
||||
const [lng, lat] = cafes[0].location.coordinates
|
||||
map.setView([lat, lng], 15)
|
||||
return
|
||||
}
|
||||
const bounds = cafes.map(
|
||||
(c) => [c.location.coordinates[1], c.location.coordinates[0]] as [number, number],
|
||||
)
|
||||
map.fitBounds(bounds, { padding: [60, 60] })
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [])
|
||||
return null
|
||||
}
|
||||
|
||||
export default function CafeMapPage() {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const cafes = (location.state as { cafes: Cafe[] } | null)?.cafes ?? []
|
||||
|
||||
const defaultCenter: [number, number] =
|
||||
cafes.length > 0
|
||||
? [cafes[0].location.coordinates[1], cafes[0].location.coordinates[0]]
|
||||
: [35.6892, 51.389]
|
||||
|
||||
return (
|
||||
<div className="relative h-screen w-screen">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => navigate(-1)}
|
||||
className="absolute right-4 top-4 z-[1000] flex h-10 w-10 items-center justify-center rounded-full bg-white shadow-lg"
|
||||
>
|
||||
<Icon name="arrowRight" className="h-5 w-5 text-gray-700" />
|
||||
</button>
|
||||
|
||||
<MapContainer
|
||||
center={defaultCenter}
|
||||
zoom={13}
|
||||
scrollWheelZoom
|
||||
zoomControl={false}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<TileLayer
|
||||
attribution="© Addwin Maps"
|
||||
url="https://maps.addwin.ir/styles/basic/{z}/{x}/{y}.png"
|
||||
maxZoom={19}
|
||||
minZoom={8}
|
||||
/>
|
||||
<ZoomControl position="topleft" />
|
||||
<FitBounds cafes={cafes} />
|
||||
{cafes.map((cafe) => (
|
||||
<Marker
|
||||
key={cafe.uuid}
|
||||
position={[cafe.location.coordinates[1], cafe.location.coordinates[0]]}
|
||||
eventHandlers={{ click: () => navigate(`/cafe/${cafe.uuid}`, { state: { cafe } }) }}
|
||||
/>
|
||||
))}
|
||||
</MapContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { MapContainer, TileLayer, Marker } from 'react-leaflet'
|
||||
import 'leaflet/dist/leaflet.css'
|
||||
import '../utils/leafletIcons'
|
||||
import Icon from '../components/Icon'
|
||||
import HomeBanner from '../components/HomeBanner'
|
||||
import { Cafe, City, fetchCafes, fetchCities, MatchEvent } from '../services/campaignApi'
|
||||
|
|
@ -11,45 +14,75 @@ function findTehran(cities: City[]): City | undefined {
|
|||
return cities.find((c) => c.city_name === 'تهران' || c.city_name.toLowerCase() === 'tehran')
|
||||
}
|
||||
|
||||
// MapIcon/ListIcon — only used by the disabled نقشه/لیست toggle above, commented out together with it
|
||||
// function MapIcon() {
|
||||
// return (
|
||||
// <svg className="h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path
|
||||
// d="M1.52734 5.186V11.6727C1.52734 12.9393 2.42734 13.4593 3.52068 12.8327L5.08734 11.9393C5.42734 11.746 5.99401 11.726 6.34734 11.906L9.84734 13.6593C10.2007 13.8327 10.7673 13.8193 11.1073 13.626L13.994 11.9727C14.3607 11.7593 14.6673 11.2393 14.6673 10.8127V4.326C14.6673 3.05933 13.7673 2.53933 12.674 3.166L11.1073 4.05933C10.7673 4.25267 10.2007 4.27267 9.84734 4.09267L6.34734 2.346C5.99401 2.17267 5.42734 2.186 5.08734 2.37933L2.20068 4.03267C1.82734 4.246 1.52734 4.766 1.52734 5.186Z"
|
||||
// stroke="currentColor"
|
||||
// strokeWidth={1.25}
|
||||
// strokeLinecap="round"
|
||||
// strokeLinejoin="round"
|
||||
// />
|
||||
// <path d="M5.70703 2.66602V11.3327" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M10.4863 4.41406V13.3341" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
||||
//
|
||||
// function ListIcon() {
|
||||
// return (
|
||||
// <svg className="h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
// <path d="M7.33398 13H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M7.33398 8.33398H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M7.33398 3.66602H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M2 3.66732L2.66667 4.33398L4.66667 2.33398" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M2 8.33333L2.66667 9L4.66667 7" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// <path d="M2 12.9993L2.66667 13.666L4.66667 11.666" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
// </svg>
|
||||
// )
|
||||
// }
|
||||
function MapIcon() {
|
||||
return (
|
||||
<svg className="h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M1.52734 5.186V11.6727C1.52734 12.9393 2.42734 13.4593 3.52068 12.8327L5.08734 11.9393C5.42734 11.746 5.99401 11.726 6.34734 11.906L9.84734 13.6593C10.2007 13.8327 10.7673 13.8193 11.1073 13.626L13.994 11.9727C14.3607 11.7593 14.6673 11.2393 14.6673 10.8127V4.326C14.6673 3.05933 13.7673 2.53933 12.674 3.166L11.1073 4.05933C10.7673 4.25267 10.2007 4.27267 9.84734 4.09267L6.34734 2.346C5.99401 2.17267 5.42734 2.186 5.08734 2.37933L2.20068 4.03267C1.82734 4.246 1.52734 4.766 1.52734 5.186Z"
|
||||
stroke="currentColor"
|
||||
strokeWidth={1.25}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<path d="M5.70703 2.66602V11.3327" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M10.4863 4.41406V13.3341" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
// MapPlaceholder — only used by the disabled نقشه/لیست toggle below (the cafes-on-a-map
|
||||
// view isn't built yet); the top-of-page بنر slot it used to fill is now HomeBanner.
|
||||
// function MapPlaceholder() {
|
||||
// return (
|
||||
// <div className="w-full h-[35vh] bg-gray-200 flex items-center justify-center text-gray-500">
|
||||
// بنر
|
||||
// </div>
|
||||
// )
|
||||
// }
|
||||
function ListIcon() {
|
||||
return (
|
||||
<svg className="h-4 w-4 shrink-0" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M7.33398 13H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M7.33398 8.33398H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M7.33398 3.66602H14.0007" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2 3.66732L2.66667 4.33398L4.66667 2.33398" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2 8.33333L2.66667 9L4.66667 7" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
<path d="M2 12.9993L2.66667 13.666L4.66667 11.666" stroke="currentColor" strokeWidth={1.25} strokeLinecap="round" strokeLinejoin="round" />
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
function CafesMapPreview({ cafes }: { cafes: Cafe[] }) {
|
||||
const navigate = useNavigate()
|
||||
const center: [number, number] =
|
||||
cafes.length > 0
|
||||
? [cafes[0].location.coordinates[1], cafes[0].location.coordinates[0]]
|
||||
: [35.6892, 51.389]
|
||||
|
||||
return (
|
||||
<div className="relative mx-4 mt-3 h-52 overflow-hidden rounded-2xl">
|
||||
<MapContainer
|
||||
center={center}
|
||||
zoom={12}
|
||||
scrollWheelZoom={false}
|
||||
dragging={false}
|
||||
zoomControl={false}
|
||||
doubleClickZoom={false}
|
||||
keyboard={false}
|
||||
className="h-full w-full"
|
||||
>
|
||||
<TileLayer
|
||||
attribution="© Addwin Maps"
|
||||
url="https://maps.addwin.ir/styles/basic/{z}/{x}/{y}.png"
|
||||
maxZoom={19}
|
||||
minZoom={8}
|
||||
/>
|
||||
{cafes.map((cafe) => (
|
||||
<Marker
|
||||
key={cafe.uuid}
|
||||
position={[cafe.location.coordinates[1], cafe.location.coordinates[0]]}
|
||||
/>
|
||||
))}
|
||||
</MapContainer>
|
||||
{/* transparent overlay — captures all clicks and opens the full-page map */}
|
||||
<div
|
||||
className="absolute inset-0 z-[1000] cursor-pointer"
|
||||
onClick={() => navigate('/map', { state: { cafes } })}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function CitySelectorRow({
|
||||
label,
|
||||
|
|
@ -205,9 +238,7 @@ function CafeCard({ cafe }: { cafe: Cafe }) {
|
|||
}
|
||||
|
||||
export default function Home() {
|
||||
// نقشه/لیست toggle — disabled for now, the map view isn't part of the app yet
|
||||
// (will be added in a later version); uncomment together with its JSX below
|
||||
// const [view, setView] = useState<'list' | 'map'>('list')
|
||||
const [view, setView] = useState<'list' | 'map'>('list')
|
||||
const [cities, setCities] = useState<City[]>([])
|
||||
const [selectedCityUuid, setSelectedCityUuid] = useState('')
|
||||
const [selectedMatchUuid, setSelectedMatchUuid] = useState('')
|
||||
|
|
@ -252,7 +283,6 @@ export default function Home() {
|
|||
|
||||
<div className="mt-6 flex items-center justify-between px-4">
|
||||
<h2 className="font-semibold text-gray-900">کافهها</h2>
|
||||
{/* نقشه/لیست toggle — commented out together with the `view` state above, see that comment
|
||||
<div className="flex rounded-full bg-gray-100 p-1">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -275,17 +305,19 @@ export default function Home() {
|
|||
لیست
|
||||
</button>
|
||||
</div>
|
||||
*/}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 px-4">
|
||||
{/* always the list view for now — restore the `view === 'list' ? ... : <MapPlaceholder />` ternary when the map toggle above is restored */}
|
||||
<div className="space-y-3">
|
||||
{cafes.map((cafe) => (
|
||||
<CafeCard key={cafe.uuid} cafe={cafe} />
|
||||
))}
|
||||
{view === 'map' ? (
|
||||
<CafesMapPreview cafes={cafes} />
|
||||
) : (
|
||||
<div className="mt-3 px-4">
|
||||
<div className="space-y-3">
|
||||
{cafes.map((cafe) => (
|
||||
<CafeCard key={cafe.uuid} cafe={cafe} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Icon from '../components/Icon'
|
|||
import { requestOtp, verifyOtp } from '../services/authApi'
|
||||
import { fetchMyCafes } from '../services/campaignApi'
|
||||
import { storeAuthTokens } from '../utils/authToken'
|
||||
import { setCafeUuid } from '../utils/cafe'
|
||||
import { clearCafeUuid, setCafeUuid } from '../utils/cafe'
|
||||
import { setUserPhone } from '../utils/userPhone'
|
||||
import { toEnglishDigits, toPersianDigits } from '../utils/formatNumber'
|
||||
import { toE164 } from '../utils/phone'
|
||||
|
|
@ -159,6 +159,7 @@ export default function LoginOtp() {
|
|||
setCafeUuid(myCafes[0].uuid)
|
||||
navigate('/owner/panel')
|
||||
} else {
|
||||
clearCafeUuid()
|
||||
navigate('/owner/register')
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -7,3 +7,7 @@ export function setCafeUuid(uuid: string) {
|
|||
export function getCafeUuid(): string | null {
|
||||
return localStorage.getItem(CAFE_UUID_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function clearCafeUuid() {
|
||||
localStorage.removeItem(CAFE_UUID_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue