diff --git a/src/assets/icons/copy.svg b/src/assets/icons/copy.svg new file mode 100644 index 0000000..8a0cd6f --- /dev/null +++ b/src/assets/icons/copy.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/components/Icon/icons.ts b/src/components/Icon/icons.ts index 2042abd..a99a3e7 100644 --- a/src/components/Icon/icons.ts +++ b/src/components/Icon/icons.ts @@ -5,6 +5,7 @@ // icons are referenced and tree-shake the rest — this file is the *only* place all 17 // 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 Book from '../../assets/icons/book.svg?react' import Calendar from '../../assets/icons/calendar.svg?react' import Call from '../../assets/icons/call.svg?react' @@ -28,6 +29,7 @@ import Success from '../../assets/icons/success.svg?react' // is used, with no second list to keep in sync. export const icons = { arrowRight: ArrowRight, + copy: Copy, book: Book, calendar: Calendar, call: Call, diff --git a/src/components/SupportButton.tsx b/src/components/SupportButton.tsx new file mode 100644 index 0000000..a54955f --- /dev/null +++ b/src/components/SupportButton.tsx @@ -0,0 +1,76 @@ +import { useState } from 'react' +import { createPortal } from 'react-dom' +import Icon from './Icon' + +const SUPPORT_PHONE = '02111111111' +const SUPPORT_PHONE_DISPLAY = '۰۲۱-۱۱۱۱۱۱۱۱' + +function isMobileDevice() { + return 'ontouchstart' in window || navigator.maxTouchPoints > 0 +} + +export default function SupportButton({ className }: { className?: string }) { + const [open, setOpen] = useState(false) + const [copied, setCopied] = useState(false) + + const handleClick = () => { + if (isMobileDevice()) { + window.location.href = `tel:${SUPPORT_PHONE}` + } else { + setOpen(true) + } + } + + const handleCopy = async () => { + try { + await navigator.clipboard.writeText(SUPPORT_PHONE) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } catch { + // silent fallback + } + } + + return ( + <> + + + {open && + createPortal( + <> +
setOpen(false)} /> +
+ +
+ +

+ {copied ? 'کپی شد!' : `شماره تماس پشتیبانی: ${SUPPORT_PHONE_DISPLAY}`} +

+
+
+ , + document.body, + )} + + ) +} diff --git a/src/pages/CafeOwnerPanel.tsx b/src/pages/CafeOwnerPanel.tsx index 60d73b0..f7a6500 100644 --- a/src/pages/CafeOwnerPanel.tsx +++ b/src/pages/CafeOwnerPanel.tsx @@ -1,6 +1,7 @@ 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' @@ -96,9 +97,7 @@ export default function CafeOwnerPanel() {
- + ) } diff --git a/src/pages/CafeOwnerRegister.tsx b/src/pages/CafeOwnerRegister.tsx index 6f296b8..d2dcd0c 100644 --- a/src/pages/CafeOwnerRegister.tsx +++ b/src/pages/CafeOwnerRegister.tsx @@ -6,6 +6,7 @@ import { MapContainer, TileLayer, Marker, ZoomControl, useMapEvents } from "reac import "leaflet/dist/leaflet.css"; import "../utils/leafletIcons"; import Icon from "../components/Icon"; +import SupportButton from "../components/SupportButton"; import { CafeFeature, CafeMediaItem, @@ -831,12 +832,7 @@ export default function CafeOwnerRegister() {
- + +

تایید ورود

- ); } @@ -84,7 +94,7 @@ export default function EventReservations() { const location = useLocation(); const navigate = useNavigate(); const [query, setQuery] = useState(""); - const [selectedIds, setSelectedIds] = useState([]); + const [togglingIds, setTogglingIds] = useState>(new Set()); const [reservations, setReservations] = useState([]); const event: CafeEvent | undefined = @@ -107,12 +117,24 @@ export default function EventReservations() { [reservations, query], ); - const toggleSelected = (reservationId: string) => { - setSelectedIds((prev) => - prev.includes(reservationId) - ? prev.filter((id) => id !== reservationId) - : [...prev, reservationId], - ); + const handleParticipate = async (reservation: EventReservation) => { + if (togglingIds.has(reservation.uuid)) return; + const newValue = !reservation.is_participated; + setTogglingIds((prev) => new Set([...prev, reservation.uuid])); + try { + await participateReservation(reservation.uuid, newValue); + setReservations((prev) => + prev.map((r) => (r.uuid === reservation.uuid ? { ...r, is_participated: newValue } : r)), + ); + } catch { + // leave checkbox at current state on failure + } finally { + setTogglingIds((prev) => { + const next = new Set(prev); + next.delete(reservation.uuid); + return next; + }); + } }; const handleDownload = () => { @@ -128,7 +150,10 @@ export default function EventReservations() { onClick={() => navigate(-1)} className="mt-4 flex w-full items-center justify-start gap-2 px-4" > - + رزروهای {event?.matchLabel ?? ""} @@ -153,8 +178,8 @@ export default function EventReservations() { toggleSelected(reservation.uuid)} + isToggling={togglingIds.has(reservation.uuid)} + onToggle={() => handleParticipate(reservation)} /> ))} diff --git a/src/pages/Home.tsx b/src/pages/Home.tsx index ddbd183..653ba65 100644 --- a/src/pages/Home.tsx +++ b/src/pages/Home.tsx @@ -1,4 +1,5 @@ import { useEffect, useRef, useState } from 'react' +import SupportButton from '../components/SupportButton' import { createPortal } from 'react-dom' import { useNavigate } from 'react-router-dom' import { MapContainer, TileLayer, Marker } from 'react-leaflet' @@ -351,12 +352,7 @@ export default function Home() { )} - + ) } diff --git a/src/services/campaignApi.ts b/src/services/campaignApi.ts index 3d60eab..01b31b1 100644 --- a/src/services/campaignApi.ts +++ b/src/services/campaignApi.ts @@ -207,6 +207,7 @@ export interface EventReservation { full_name: string phone_number: string status: string + is_participated: boolean created_at: string } @@ -215,6 +216,10 @@ export async function fetchEventReservations(cafeUuid: string, eventUuid: string return data } +export async function participateReservation(uuid: string, isParticipated: boolean): Promise { + await api.patch(`/campaign/reservations/${uuid}/participate/`, { is_participated: isParticipated }) +} + export interface MyReservation { uuid: string reservation_code: string