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