feature/WNPT-301 #6
8 changed files with 167 additions and 35 deletions
5
src/assets/icons/logout.svg
Normal file
5
src/assets/icons/logout.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg width="18" height="18" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M6.67578 5.66969C6.90828 2.96969 8.29578 1.86719 11.3333 1.86719H11.4308C14.7833 1.86719 16.1258 3.20969 16.1258 6.56219V11.4522C16.1258 14.8047 14.7833 16.1472 11.4308 16.1472H11.3333C8.31828 16.1472 6.93078 15.0597 6.68328 12.4047" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11.2498 9H2.71484" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M4.3875 6.48828L1.875 9.00078L4.3875 11.5133" stroke="#333333" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 685 B |
|
|
@ -6,6 +6,7 @@
|
|||
// icons are ever imported, so an icon that no call site uses never makes it into a chunk.
|
||||
import ArrowRight from '../../assets/icons/arrow-right.svg?react'
|
||||
import Copy from '../../assets/icons/copy.svg?react'
|
||||
import Logout from '../../assets/icons/logout.svg?react'
|
||||
import Book from '../../assets/icons/book.svg?react'
|
||||
import Calendar from '../../assets/icons/calendar.svg?react'
|
||||
import Call from '../../assets/icons/call.svg?react'
|
||||
|
|
@ -30,6 +31,7 @@ import Success from '../../assets/icons/success.svg?react'
|
|||
export const icons = {
|
||||
arrowRight: ArrowRight,
|
||||
copy: Copy,
|
||||
logout: Logout,
|
||||
book: Book,
|
||||
calendar: Calendar,
|
||||
call: Call,
|
||||
|
|
|
|||
29
src/components/LogoutModal.tsx
Normal file
29
src/components/LogoutModal.tsx
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
import { createPortal } from 'react-dom'
|
||||
|
||||
export default function LogoutModal({ onConfirm, onCancel }: { onConfirm: () => void; onCancel: () => void }) {
|
||||
return createPortal(
|
||||
<>
|
||||
<div className="fixed inset-0 z-9998 bg-black/40" onClick={onCancel} />
|
||||
<div className="fixed inset-x-0 bottom-0 z-9999 rounded-t-2xl bg-white px-5 pb-8 pt-6 shadow-xl">
|
||||
<p className="mb-6 text-center font-semibold text-gray-900">میخواهید از حساب کاربری خود خارج شوید؟</p>
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onConfirm}
|
||||
className="flex-1 rounded-xl bg-[#223D5D] py-3 font-semibold text-white"
|
||||
>
|
||||
بله
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onCancel}
|
||||
className="flex-1 rounded-xl border border-[#223D5D] py-3 font-semibold text-gray-900"
|
||||
>
|
||||
خیر
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</>,
|
||||
document.body,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,81 +1,136 @@
|
|||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import Icon from '../components/Icon'
|
||||
import SupportButton from '../components/SupportButton'
|
||||
import { useNow } from '../hooks/useNow'
|
||||
import { CafeSchedule, fetchCafeSchedules } from '../services/campaignApi'
|
||||
import { getCafeUuid } from '../utils/cafe'
|
||||
import HostBanner from '../assets/images/banners/Host-banner.jpg'
|
||||
import { formatPersianNumber } from '../utils/formatNumber'
|
||||
import { formatMatchLabel } from '../utils/matchCountdown'
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import Icon from "../components/Icon";
|
||||
import SupportButton from "../components/SupportButton";
|
||||
import LogoutModal from "../components/LogoutModal";
|
||||
import { logout } from "../utils/logout";
|
||||
import { useNow } from "../hooks/useNow";
|
||||
import { CafeSchedule, fetchCafeSchedules } from "../services/campaignApi";
|
||||
import { getCafeUuid } from "../utils/cafe";
|
||||
import HostBanner from "../assets/images/banners/Host-banner.jpg";
|
||||
import { formatPersianNumber } from "../utils/formatNumber";
|
||||
import { formatMatchLabel } from "../utils/matchCountdown";
|
||||
|
||||
function PanelRow({ icon, label, onClick }: { icon: ReactNode; label: string; onClick?: () => void }) {
|
||||
function PanelRow({
|
||||
icon,
|
||||
label,
|
||||
onClick,
|
||||
}: {
|
||||
icon: ReactNode;
|
||||
label: string;
|
||||
onClick?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button type="button" onClick={onClick} className="flex w-full items-center justify-between py-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={onClick}
|
||||
className="flex w-full items-center justify-between py-4"
|
||||
>
|
||||
<div className="flex items-center gap-2">
|
||||
{icon}
|
||||
<span className="text-gray-900">{label}</span>
|
||||
</div>
|
||||
<Icon name="arrowRight" className="h-5 w-5 text-gray-400" />
|
||||
</button>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default function CafeOwnerPanel() {
|
||||
const navigate = useNavigate()
|
||||
const now = useNow()
|
||||
const [schedules, setSchedules] = useState<CafeSchedule[]>([])
|
||||
const navigate = useNavigate();
|
||||
const now = useNow();
|
||||
const [schedules, setSchedules] = useState<CafeSchedule[]>([]);
|
||||
const [showLogout, setShowLogout] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const cafeUuid = getCafeUuid()
|
||||
if (!cafeUuid) return
|
||||
const cafeUuid = getCafeUuid();
|
||||
if (!cafeUuid) return;
|
||||
fetchCafeSchedules(cafeUuid)
|
||||
.then(setSchedules)
|
||||
.catch(() => {})
|
||||
}, [])
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pb-20">
|
||||
<img src={HostBanner} alt="" className="w-full" />
|
||||
<div className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLogout(true)}
|
||||
className="absolute left-3 top-3 flex items-center gap-1.5 rounded-full bg-white/90 px-3 py-1.5 text-sm font-semibold text-gray-800 shadow"
|
||||
>
|
||||
<Icon name="logout" className="h-4 w-4 font-medium" />
|
||||
خروج
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showLogout && (
|
||||
<LogoutModal
|
||||
onConfirm={() => {
|
||||
logout();
|
||||
navigate("/cafe/login");
|
||||
}}
|
||||
onCancel={() => setShowLogout(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="px-4">
|
||||
<h1 className="mt-4 text-right text-lg font-bold text-gray-900">پنل کافه</h1>
|
||||
<h1 className="mt-4 text-right text-lg font-bold text-gray-900">
|
||||
پنل کافه
|
||||
</h1>
|
||||
|
||||
<div className="mt-2 divide-y divide-gray-100">
|
||||
<PanelRow
|
||||
icon={<Icon name="profileCircle" className="h-5 w-5 text-[#4D4D4D]" />}
|
||||
icon={
|
||||
<Icon name="profileCircle" className="h-5 w-5 text-[#4D4D4D]" />
|
||||
}
|
||||
label="اطلاعات کافه"
|
||||
onClick={() => navigate('/owner/register', { state: { step: 2 } })}
|
||||
onClick={() => navigate("/owner/register", { state: { step: 2 } })}
|
||||
/>
|
||||
<PanelRow
|
||||
icon={<Icon name="book" className="h-5 w-5 text-[#4D4D4D]" />}
|
||||
label="افزودن رویداد"
|
||||
onClick={() => navigate("/owner/events/new")}
|
||||
/>
|
||||
<PanelRow icon={<Icon name="book" className="h-5 w-5 text-[#4D4D4D]" />} label="افزودن رویداد" onClick={() => navigate('/owner/events/new')} />
|
||||
</div>
|
||||
|
||||
<h2 className="mt-6 text-right font-semibold text-gray-900">رویدادهای فعال شما</h2>
|
||||
<h2 className="mt-6 text-right font-semibold text-gray-900">
|
||||
رویدادهای فعال شما
|
||||
</h2>
|
||||
|
||||
<div className="mt-3 flex flex-col gap-6">
|
||||
{schedules.map((schedule) => {
|
||||
const isPast = new Date(schedule.match_event.match_datetime).getTime() <= now.getTime()
|
||||
const matchLabel = formatMatchLabel(schedule.match_event, now)
|
||||
const isPast =
|
||||
new Date(schedule.match_event.match_datetime).getTime() <=
|
||||
now.getTime();
|
||||
const matchLabel = formatMatchLabel(schedule.match_event, now);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={schedule.uuid}
|
||||
className={`rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F] ${isPast ? 'opacity-50' : ''}`}
|
||||
className={`rounded-2xl px-4 shadow-[0px_4px_12px_0px_#0000001F] ${isPast ? "opacity-50" : ""}`}
|
||||
>
|
||||
<div className="pt-4">
|
||||
<div className="flex items-center justify-between pb-3">
|
||||
<p className="text-right text-gray-900">{matchLabel}</p>
|
||||
{isPast && (
|
||||
<span className="shrink-0 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500">برگزار شده</span>
|
||||
<span className="shrink-0 rounded-full bg-gray-100 px-3 py-1 text-xs text-gray-500">
|
||||
برگزار شده
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="mt-2 inline-block rounded-full bg-[#ECF2F8] px-3 py-1 text-sm text-gray-700">
|
||||
{formatPersianNumber(schedule.remaining_tickets)} صندلی از {formatPersianNumber(schedule.total_tickets)} باقی مانده
|
||||
{formatPersianNumber(schedule.remaining_tickets)} صندلی از{" "}
|
||||
{formatPersianNumber(schedule.total_tickets)} باقی مانده
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<PanelRow
|
||||
icon={<Icon name="calendar" className="h-5 w-5 text-[#4D4D4D]" />}
|
||||
icon={
|
||||
<Icon
|
||||
name="calendar"
|
||||
className="h-5 w-5 text-[#4D4D4D]"
|
||||
/>
|
||||
}
|
||||
label="مشاهده رزروها"
|
||||
onClick={() =>
|
||||
navigate(`/owner/events/${schedule.uuid}/reservations`, {
|
||||
|
|
@ -92,12 +147,12 @@ export default function CafeOwnerPanel() {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SupportButton className="fixed bottom-4 right-4 rounded-full bg-[#223D5D] px-5 py-3 font-semibold text-white shadow-lg" />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { useEffect, useRef, useState } from 'react'
|
||||
import SupportButton from '../components/SupportButton'
|
||||
import LogoutModal from '../components/LogoutModal'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { MapContainer, TileLayer, Marker } from 'react-leaflet'
|
||||
|
|
@ -7,6 +8,7 @@ import 'leaflet/dist/leaflet.css'
|
|||
import '../utils/leafletIcons'
|
||||
import Icon from '../components/Icon'
|
||||
import HomeBanner from '../components/HomeBanner'
|
||||
import { logout } from '../utils/logout'
|
||||
import { Cafe, City, fetchCafes, fetchCities, MatchEvent } from '../services/campaignApi'
|
||||
import { useMatchEvents } from '../hooks/useMatchEvents'
|
||||
import { formatMatchLabel } from '../utils/matchCountdown'
|
||||
|
|
@ -79,7 +81,7 @@ function CafesMapPreview({ cafes }: { cafes: Cafe[] }) {
|
|||
</MapContainer>
|
||||
{/* transparent overlay — captures all clicks and opens the full-page map */}
|
||||
<div
|
||||
className="absolute inset-0 z-[1000] cursor-pointer"
|
||||
className="absolute inset-0 z-1000 cursor-pointer"
|
||||
onClick={() => navigate('/map', { state: { cafes } })}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -271,10 +273,12 @@ function CafeCard({ cafe }: { cafe: Cafe }) {
|
|||
}
|
||||
|
||||
export default function Home() {
|
||||
const navigate = useNavigate()
|
||||
const [view, setView] = useState<'list' | 'map'>('list')
|
||||
const [cities, setCities] = useState<City[]>([])
|
||||
const [selectedCityUuid, setSelectedCityUuid] = useState('')
|
||||
const [selectedMatchUuid, setSelectedMatchUuid] = useState('')
|
||||
const [showLogout, setShowLogout] = useState(false)
|
||||
const { matches, now } = useMatchEvents()
|
||||
const [cafes, setCafes] = useState<Cafe[]>([])
|
||||
|
||||
|
|
@ -301,7 +305,24 @@ export default function Home() {
|
|||
|
||||
return (
|
||||
<div className="min-h-screen bg-white pb-24">
|
||||
<HomeBanner />
|
||||
<div className="relative">
|
||||
<HomeBanner />
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowLogout(true)}
|
||||
className="absolute left-3 top-3 flex items-center gap-1.5 rounded-full bg-white/90 px-3 py-1.5 text-sm font-semibold text-gray-800 shadow"
|
||||
>
|
||||
<Icon name="logout" className="h-4 w-4" />
|
||||
خروج
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showLogout && (
|
||||
<LogoutModal
|
||||
onConfirm={() => { logout(); navigate('/login') }}
|
||||
onCancel={() => setShowLogout(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
<MyReservationsRow />
|
||||
|
||||
|
|
|
|||
12
src/utils/logout.ts
Normal file
12
src/utils/logout.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { clearAuthTokens } from './authToken'
|
||||
import { clearCafeUuid } from './cafe'
|
||||
import { clearUserRole } from './role'
|
||||
import { clearUserPhone } from './userPhone'
|
||||
|
||||
export function logout() {
|
||||
clearAuthTokens()
|
||||
clearUserRole()
|
||||
clearUserPhone()
|
||||
clearCafeUuid()
|
||||
sessionStorage.clear()
|
||||
}
|
||||
|
|
@ -10,3 +10,7 @@ export function setUserRole(role: UserRole) {
|
|||
export function getUserRole(): UserRole {
|
||||
return localStorage.getItem(ROLE_STORAGE_KEY) === 'cafe_owner' ? 'cafe_owner' : 'customer'
|
||||
}
|
||||
|
||||
export function clearUserRole() {
|
||||
localStorage.removeItem(ROLE_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,3 +7,7 @@ export function setUserPhone(phone: string) {
|
|||
export function getUserPhone(): string | null {
|
||||
return localStorage.getItem(USER_PHONE_STORAGE_KEY)
|
||||
}
|
||||
|
||||
export function clearUserPhone() {
|
||||
localStorage.removeItem(USER_PHONE_STORAGE_KEY)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue