feature/WNPT-301 #6

Merged
afra.khalaj merged 6 commits from feature/WNPT-301 into develop 2026-07-25 04:03:40 -04:00
8 changed files with 167 additions and 35 deletions
Showing only changes of commit 2e26ef067f - Show all commits

View 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

View file

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

View 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,
)
}

View file

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

View file

@ -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
View 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()
}

View file

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

View file

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