diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..2435cdda --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git + +.firebase + +.editorconfig + +/node_modules + +/e2e + +/docs + +.gitignore + +*.zip + +*.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..3423715d --- /dev/null +++ b/Dockerfile @@ -0,0 +1,21 @@ +FROM node:22 as build + +WORKDIR /app + +RUN npm install -g pnpm + +COPY package*.json . + +RUN pnpm install + +COPY . . + +RUN npm run build + +FROM nginx:latest + +COPY ./nginx.conf /etc/nginx/conf.d/default.conf + +COPY --from=build /app/dist /usr/share/nginx/html + +EXPOSE 80 \ No newline at end of file diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 00000000..ea96b55c --- /dev/null +++ b/nginx.conf @@ -0,0 +1,9 @@ +server { + listen 80; + server_name localhost; + root /usr/share/nginx/html; + index index.html; + location / { + try_files $uri $uri/ /index.html; + } +} \ No newline at end of file diff --git a/src/apps/appointment/layouts/TopNavLayout.tsx b/src/apps/appointment/layouts/TopNavLayout.tsx index 5e75a76e..fce20d6e 100644 --- a/src/apps/appointment/layouts/TopNavLayout.tsx +++ b/src/apps/appointment/layouts/TopNavLayout.tsx @@ -13,6 +13,9 @@ const TopNavLayout = () => { [appointmentRoutes.faq]: appointmentRoutes.explore, [appointmentRoutes.loyaltyCards]: appointmentRoutes.profile, [appointmentRoutes.CalendarManagement]: appointmentRoutes.explore, + [appointmentRoutes.customersEdit]: appointmentRoutes.customers, + [appointmentRoutes.addCustomers]: appointmentRoutes.customers, + [appointmentRoutes.customers]: appointmentRoutes.explore || appointmentRoutes.profile, }; const handleBack = () => { diff --git a/src/apps/new-ui/assets/rose-assistant/چهره خسته.webp b/src/apps/new-ui/assets/rose-assistant/exhaustedRose.webp similarity index 100% rename from src/apps/new-ui/assets/rose-assistant/چهره خسته.webp rename to src/apps/new-ui/assets/rose-assistant/exhaustedRose.webp diff --git a/src/apps/new-ui/assets/rose-assistant/فرم در دست.webp b/src/apps/new-ui/assets/rose-assistant/formRose.webp similarity index 100% rename from src/apps/new-ui/assets/rose-assistant/فرم در دست.webp rename to src/apps/new-ui/assets/rose-assistant/formRose.webp diff --git a/src/apps/new-ui/assets/rose-assistant/دست ها به بالا.webp b/src/apps/new-ui/assets/rose-assistant/happyRose.webp similarity index 100% rename from src/apps/new-ui/assets/rose-assistant/دست ها به بالا.webp rename to src/apps/new-ui/assets/rose-assistant/happyRose.webp diff --git a/src/apps/new-ui/assets/rose-assistant/نگاه به ناخن ها.webp b/src/apps/new-ui/assets/rose-assistant/nailRose.webp similarity index 100% rename from src/apps/new-ui/assets/rose-assistant/نگاه به ناخن ها.webp rename to src/apps/new-ui/assets/rose-assistant/nailRose.webp diff --git a/src/apps/new-ui/assets/rose-assistant/تابلو به دست.webp b/src/apps/new-ui/assets/rose-assistant/sadRose.webp similarity index 100% rename from src/apps/new-ui/assets/rose-assistant/تابلو به دست.webp rename to src/apps/new-ui/assets/rose-assistant/sadRose.webp diff --git a/src/apps/new-ui/pages/Advertisement/index.tsx b/src/apps/new-ui/pages/Advertisement/index.tsx index 260ca290..5c0bf63a 100644 --- a/src/apps/new-ui/pages/Advertisement/index.tsx +++ b/src/apps/new-ui/pages/Advertisement/index.tsx @@ -28,18 +28,19 @@ const Advertisement = () => { pauseOnMouseEnter: false, }} slidesPerView="auto" - spaceBetween={8} + spaceBetween={8} centeredSlides={false} > {(banners as Banner[])?.map((banner, index) => ( -
+
{`eventBanner { const navigate = useNavigate(); return (
-
+
{isLoading ? ( -
در حال بارگذاری...
+
+ در حال بارگذاری... +
) : isError ? ( -
خطا در دریافت بنرها
+
+ خطا در دریافت بنرها +
) : ( { disableOnInteraction: false, pauseOnMouseEnter: false, }} - className="py-5" + className="pt-10 -mt-10" slidesPerView="auto" spaceBetween={10} - centeredSlides={true} - + centeredSlides={false} > {/* first slide-rose */}
@@ -76,11 +80,12 @@ const BannerSwiper = () => { -
+
{`eventBanner { - const [search, setSearch] = useState(""); - const [open, setOpen] = useState(false); + const [search, setSearch] = useState(""); + const [open, setOpen] = useState(false); - const { data: unreadData } = useGetUnreadNotificationCount(); + const { data: unreadData } = useGetUnreadNotificationCount(); - const { data: listData, isLoading } = useGetUserNotifications(open); + const { data: listData, isLoading } = useGetUserNotifications(open); - const markSeen = useMarkNotificationSeen(); - const navigate = useNavigate(); + const markSeen = useMarkNotificationSeen(); + const navigate = useNavigate(); - const [localItems, setLocalItems] = useState([]); - const [removingIds, setRemovingIds] = useState>(new Set()); - const [localSeenIds, setLocalSeenIds] = useState>(new Set()); + const [localItems, setLocalItems] = useState([]); + const [removingIds, setRemovingIds] = useState>(new Set()); + const [localSeenIds, setLocalSeenIds] = useState>(new Set()); - const [translateMap, setTranslateMap] = useState>({}); + const [translateMap, setTranslateMap] = useState>({}); - const draggingIdRef = useRef(null); - const startXRef = useRef>(new Map()); + const draggingIdRef = useRef(null); + const startXRef = useRef>(new Map()); - const timersRef = useRef([]); - useEffect(() => { - return () => { - timersRef.current.forEach((t) => clearTimeout(t)); - timersRef.current = []; - }; - }, []); + const timersRef = useRef([]); + useEffect(() => { + return () => { + timersRef.current.forEach((t) => clearTimeout(t)); + timersRef.current = []; + }; + }, []); - useEffect(() => { - const server = listData?.notifications ?? []; - setLocalItems((prev) => { - if (!prev.length) return server.filter((s) => !localSeenIds.has(s.id)); + // useEffect(() => { + // const server = listData?.notifications ?? []; + // setLocalItems((prev) => { + // if (!prev.length) return server.filter((s) => !localSeenIds.has(s.id)); - const kept = prev.filter((p) => removingIds.has(p.id)); - const merged = server.filter((s) => !removingIds.has(s.id) && !localSeenIds.has(s.id)).concat(kept); - return merged; - }); - }, [listData, removingIds, localSeenIds]); + // const kept = prev.filter((p) => removingIds.has(p.id)); + // const merged = server.filter((s) => !removingIds.has(s.id) && !localSeenIds.has(s.id)).concat(kept); + // return merged; + // }); + // }, [listData, removingIds, localSeenIds]); - const filtered = useMemo(() => { - const s = search.trim().toLowerCase(); - if (!s) return localItems; - return localItems.filter((n) => { - return (n.title || "").toLowerCase().includes(s) || (n.content || "").toLowerCase().includes(s); - }); - }, [localItems, search]); + useEffect(() => { + const server = listData?.notifications ?? []; - const ANIM_DURATION = 300; - const DISMISS_THRESHOLD = 80; + // آیتم‌هایی که باید حذف بشن + const blocked = new Set([ + ...Array.from(localSeenIds), + ...Array.from(removingIds), + ]); - const removeItemLocally = (id: number) => { - setLocalItems((prev) => prev.filter((x) => x.id !== id)); - setTranslateMap((prev) => { - const next = { ...prev }; - delete next[id]; - return next; - }); - setRemovingIds((prev) => { + setLocalItems(server.filter((s) => !blocked.has(s.id) && !s.seen)); + }, [listData, removingIds, localSeenIds]); + + const filtered = useMemo(() => { + const s = search.trim().toLowerCase(); + if (!s) return localItems; + return localItems.filter((n) => { + return ( + (n.title || "").toLowerCase().includes(s) || + (n.content || "").toLowerCase().includes(s) + ); + }); + }, [localItems, search]); + + const ANIM_DURATION = 300; + const DISMISS_THRESHOLD = 80; + + const removeItemLocally = (id: number) => { + setLocalItems((prev) => prev.filter((x) => x.id !== id)); + setTranslateMap((prev) => { + const next = { ...prev }; + delete next[id]; + return next; + }); + setRemovingIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + setLocalSeenIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + }; + + const startDrag = (e: React.PointerEvent, id: number) => { + const target = e.target as HTMLElement; + if (target.tagName === "BUTTON" || target.closest("button")) return; + + draggingIdRef.current = id; + startXRef.current.set(id, e.clientX); + try { + (e.currentTarget as Element).setPointerCapture(e.pointerId); + } catch {} + }; + + const onPointerMove = (e: React.PointerEvent, id: number) => { + const start = startXRef.current.get(id); + if (start == null) return; + const delta = e.clientX - start; + setTranslateMap((prev) => { + if (prev[id] === delta) return prev; + return { ...prev, [id]: delta }; + }); + }; + + const endDrag = (e: React.PointerEvent, n: Notification) => { + const id = n.id; + const start = startXRef.current.get(id); + if (start == null) return; + const delta = e.clientX - start; + try { + (e.currentTarget as Element).releasePointerCapture(e.pointerId); + } catch {} + startXRef.current.delete(id); + draggingIdRef.current = null; + + if (Math.abs(delta) >= DISMISS_THRESHOLD) { + const sign = delta > 0 ? 1 : -1; + const offX = sign * (window.innerWidth || 800); + setTranslateMap((prev) => ({ ...prev, [id]: offX })); + setRemovingIds((prev) => new Set(prev).add(id)); + setLocalSeenIds((prev) => new Set(prev).add(id)); + + markSeen.mutate(id, { + onError: () => { + setRemovingIds((prev) => { const next = new Set(prev); next.delete(id); return next; + }); + setLocalSeenIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; + }); + setTranslateMap((prev) => ({ ...prev, [id]: 0 })); + }, + }); + + const t = window.setTimeout(() => { + removeItemLocally(id); + }, ANIM_DURATION + 30); + timersRef.current.push(t); + } else { + setTranslateMap((prev) => ({ ...prev, [id]: 0 })); + } + }; + + const handleAcknowledge = (n: Notification) => { + const id = n.id; + if (removingIds.has(id)) return; + const sign = 1; + const offX = sign * (window.innerWidth || 800); + setTranslateMap((prev) => ({ ...prev, [id]: offX })); + setRemovingIds((prev) => new Set(prev).add(id)); + setLocalSeenIds((prev) => new Set(prev).add(id)); + + markSeen.mutate(id, { + onError: () => { + setRemovingIds((prev) => { + const next = new Set(prev); + next.delete(id); + return next; }); setLocalSeenIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; + const next = new Set(prev); + next.delete(id); + return next; }); - }; + setTranslateMap((prev) => ({ ...prev, [id]: 0 })); + }, + }); - const startDrag = (e: React.PointerEvent, id: number) => { - const target = e.target as HTMLElement; - if (target.tagName === "BUTTON" || target.closest("button")) return; + const t = window.setTimeout(() => { + removeItemLocally(id); + }, ANIM_DURATION + 30); + timersRef.current.push(t); + }; - draggingIdRef.current = id; - startXRef.current.set(id, e.clientX); - try { - (e.currentTarget as Element).setPointerCapture(e.pointerId); - } catch {} - }; + // render + const unreadCount = unreadData?.unread_count ?? 0; - const onPointerMove = (e: React.PointerEvent, id: number) => { - const start = startXRef.current.get(id); - if (start == null) return; - const delta = e.clientX - start; - setTranslateMap((prev) => { - if (prev[id] === delta) return prev; - return { ...prev, [id]: delta }; - }); - }; + return ( + <> + {/* wrap img so we can position a badge */} +
+ alert icon setOpen(true)} + className="cursor-pointer" + /> - const endDrag = (e: React.PointerEvent, n: Notification) => { - const id = n.id; - const start = startXRef.current.get(id); - if (start == null) return; - const delta = e.clientX - start; - try { - (e.currentTarget as Element).releasePointerCapture(e.pointerId); - } catch {} - startXRef.current.delete(id); - draggingIdRef.current = null; + {unreadCount > 0 && ( + + {unreadCount > 99 ? "99+" : unreadCount} + + )} +
- if (Math.abs(delta) >= DISMISS_THRESHOLD) { - const sign = delta > 0 ? 1 : -1; - const offX = sign * (window.innerWidth || 800); - setTranslateMap((prev) => ({ ...prev, [id]: offX })); - setRemovingIds((prev) => new Set(prev).add(id)); - setLocalSeenIds((prev) => new Set(prev).add(id)); + {open && ( +
+
setOpen(false)} + className="absolute inset-0 pointer-events-auto" + style={{ + background: "rgba(0,0,0,0.35)", + backdropFilter: "blur(8px)", + WebkitBackdropFilter: "blur(8px)", + }} + /> - markSeen.mutate(id, { - onError: () => { - setRemovingIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - setLocalSeenIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - setTranslateMap((prev) => ({ ...prev, [id]: 0 })); - }, - }); +
e.stopPropagation()} + > +
+ setOpen(false)} /> - const t = window.setTimeout(() => { - removeItemLocally(id); - }, ANIM_DURATION + 30); - timersRef.current.push(t); - } else { - setTranslateMap((prev) => ({ ...prev, [id]: 0 })); - } - }; - - const handleAcknowledge = (n: Notification) => { - const id = n.id; - if (removingIds.has(id)) return; - const sign = 1; - const offX = sign * (window.innerWidth || 800); - setTranslateMap((prev) => ({ ...prev, [id]: offX })); - setRemovingIds((prev) => new Set(prev).add(id)); - setLocalSeenIds((prev) => new Set(prev).add(id)); - - markSeen.mutate(id, { - onError: () => { - setRemovingIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - setLocalSeenIds((prev) => { - const next = new Set(prev); - next.delete(id); - return next; - }); - setTranslateMap((prev) => ({ ...prev, [id]: 0 })); - }, - }); - - const t = window.setTimeout(() => { - removeItemLocally(id); - }, ANIM_DURATION + 30); - timersRef.current.push(t); - }; - - // render - const unreadCount = unreadData?.unread_count ?? 0; - - return ( - <> - {/* wrap img so we can position a badge */} -
- alert icon setOpen(true)} className="cursor-pointer" /> - - {unreadCount > 0 && ( - - {unreadCount > 99 ? "99+" : unreadCount} - - )} + setSearch(e.target.value)} + placeholder="جستجو..." + maxLength={25} + className="flex-1 px-4 py-2 rounded-full bg-white placeholder:text-gray-400 text-gray-900 outline-none border border-gray-200" + aria-label="جستجو اعلان" + />
- {open && ( -
-
setOpen(false)} - className="absolute inset-0 pointer-events-auto" - style={{ - background: "rgba(0,0,0,0.35)", - backdropFilter: "blur(8px)", - WebkitBackdropFilter: "blur(8px)", - }} - /> +
+ خوانده‌ نشده: {unreadCount} +
-
e.stopPropagation()} - > -
- setOpen(false)} /> - - setSearch(e.target.value)} - placeholder="جستجو..." - maxLength={25} - className="flex-1 px-4 py-2 rounded-full bg-white placeholder:text-gray-400 text-gray-900 outline-none border border-gray-200" - aria-label="جستجو اعلان" - /> -
- -
خوانده‌ نشده: {unreadCount}
- -
    - {isLoading &&
    درحال بارگذاری...
    } - {!isLoading && filtered.length === 0 && ( -
    اعلانی یافت نشد
    - )} - - {filtered.map((n) => { - if(n.seen) return null; - const buttons = safeParseButtons(n.buttons) as Array<{ title: string; url: string }>; - const isRemoving = removingIds.has(n.id); - const translate = translateMap[n.id] ?? 0; - const isDragging = draggingIdRef.current === n.id; - - return ( -
  • -
    startDrag(e, n.id)} - onPointerMove={(e) => onPointerMove(e, n.id)} - onPointerUp={(e) => endDrag(e, n)} - onPointerCancel={(e) => endDrag(e as any, n)} - className={`flex justify-between items-start p-3 rounded-xl shadow-sm border border-gray-100`} - style={{ - background: "white", - transform: `translateX(${translate}px)`, - transition: isDragging - ? "none" - : `transform ${ANIM_DURATION}ms ease, opacity ${ANIM_DURATION}ms ease`, - opacity: isRemoving ? 0 : 1, - touchAction: "pan-y", - }} - > -
    -
    -

    {n.title}

    - -
    -

    {n.content}

    - - {buttons.length > 0 && ( -
    - {buttons.map((b, idx) => { - const handleClick = (ev?: React.MouseEvent) => { - ev?.stopPropagation(); - if (b.url === "quit") { - handleAcknowledge(n); - } else if (/^https?:\/\//i.test(b.url)) { - window.open(b.url, "_blank"); - } else { - navigate(b.url); - } - }; - - return ( - - ); - })} -
    - )} - - {!buttons.some((b) => b.url === "quit") && ( -
    - -
    - )} -
    - -
    - {!n.seen && ( - - )} -
    -
    -
  • - ); - })} -
-
+
    + {isLoading && ( +
    درحال بارگذاری...
    + )} + {!isLoading && filtered.length === 0 && ( +
    + اعلانی یافت نشد
    - )} - - ); + )} + + {filtered.map((n) => { + if (n.seen) return null; + const buttons = safeParseButtons(n.buttons) as Array<{ + title: string; + url: string; + }>; + const isRemoving = removingIds.has(n.id); + const translate = translateMap[n.id] ?? 0; + const isDragging = draggingIdRef.current === n.id; + + return ( +
  • +
    startDrag(e, n.id)} + onPointerMove={(e) => onPointerMove(e, n.id)} + onPointerUp={(e) => endDrag(e, n)} + onPointerCancel={(e) => endDrag(e as any, n)} + className={`flex justify-between items-start p-3 rounded-xl shadow-sm border border-gray-100`} + style={{ + background: "white", + transform: `translateX(${translate}px)`, + transition: isDragging + ? "none" + : `transform ${ANIM_DURATION}ms ease, opacity ${ANIM_DURATION}ms ease`, + opacity: isRemoving ? 0 : 1, + touchAction: "pan-y", + }} + > +
    +
    +

    + {n.title} +

    + +
    +

    + {n.content} +

    + + {buttons.length > 0 && ( +
    + {buttons.map((b, idx) => { + const handleClick = (ev?: React.MouseEvent) => { + ev?.stopPropagation(); + if (b.url === "quit") { + handleAcknowledge(n); + } else if (/^https?:\/\//i.test(b.url)) { + window.open(b.url, "_blank"); + } else { + navigate(b.url); + } + }; + + return ( + + ); + })} +
    + )} + + {!buttons.some((b) => b.url === "quit") && ( +
    + +
    + )} +
    + +
    + {!n.seen && ( + + )} +
    +
    +
  • + ); + })} +
+
+
+ )} + + ); }; export default NotificationsPhoneStyle; diff --git a/src/apps/new-ui/pages/rose-assistant/index.tsx b/src/apps/new-ui/pages/rose-assistant/index.tsx index 3f921222..fdcac438 100644 --- a/src/apps/new-ui/pages/rose-assistant/index.tsx +++ b/src/apps/new-ui/pages/rose-assistant/index.tsx @@ -1,12 +1,12 @@ import { ErrorComponentFallback } from "@/components/ErrorComponentFallback"; import { Skeleton } from "@/components/ui/skeleton"; import textBubble from "../../assets/rose-assistant/bubble.png"; -import roseIcon_1 from "../../assets/rose-assistant/rose1.png"; -import roseIcon_2 from "../../assets/rose-assistant/چهره خسته.webp"; -import roseIcon_3 from "../../assets/rose-assistant/فرم در دست.webp"; -import roseIcon_4 from "../../assets/rose-assistant/دست ها به بالا.webp"; -import roseIcon_5 from "../../assets/rose-assistant/تابلو به دست.webp"; -import roseIcon_6 from "../../assets/rose-assistant/نگاه به ناخن ها.webp"; +import roseIcon_1 from "../../assets/rose-assistant/Rose1.png"; +import roseIcon_2 from "../../assets/rose-assistant/exhaustedRose.webp"; +import roseIcon_3 from "../../assets/rose-assistant/formRose.webp"; +import roseIcon_4 from "../../assets/rose-assistant/happyRose.webp"; +import roseIcon_5 from "../../assets/rose-assistant/sadRose.webp"; +import roseIcon_6 from "../../assets/rose-assistant/nailRose.webp"; import moduleRose from "../../assets/modules/moduleRose.webp"; import bgRose from "../../assets/rose-assistant/bgRose.png"; import roseBusyDays from "../../assets/rose-assistant/busyIcon.svg";