Vitron-Front/app/routes/store.$storeId.setting.tsx
Arda Samadi fef53fd003 feat(notifications): web push opt-in + unread nav badges
Surfaces three events without a full notification center:
- service worker (public/sw.js) gains push + notificationclick handlers
  (RTL, focuses an existing tab or opens the target URL).
- usePushNotifications hook: permission + VAPID subscribe/unsubscribe
  against the backend push endpoints.
- opt-in UI both ways: a one-time dismissible post-login banner
  (PushPermissionBanner, rendered in MyLayout) and a permanent toggle
  (PushSettingToggle) on buyer profile settings and seller store settings.
- useBadgeCounts polls /api/notif/v1/badges/; small red badges on the
  bottom-nav profile icon (chat + order updates), the profile sidebar
  (پیام‌ها / سفارش‌ها) and the seller dashboard sidebar. Visiting orders
  clears its badge.

Degrades gracefully: if the backend endpoints are absent the counts fall
back to zero and nothing throws.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 14:02:56 +03:30

284 lines
10 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { MetaFunction, useNavigate, useParams } from "@remix-run/react";
import { useSellerData } from "../requestHandler/use-seller-hooks";
import {
ChevronLeft,
LogOut,
LayoutDashboard,
ShoppingCart,
Truck,
PackagePlus,
TableProperties,
PenLine,
Code,
Instagram,
MessageCircle,
} from "lucide-react";
import HeaderWithSupport from "../components/HeaderWithSupport";
import { useStoreOwnership } from "../hooks/useStoreOwnership";
import { useState } from "react";
import { SunMoonToggle } from "~/components/SunMoonToggle";
import { useRootData } from "~/hooks/use-root-data";
import { PushSettingToggle } from "~/components/notifications/PushSettingToggle";
// Import the InstagramSyncDrawer component
import { InstagramSyncDrawer } from "./store.$storeId._index";
import CallDialog from "~/components/CallDialog";
interface MenuItemType {
title: string;
link?: string;
pageName: string;
hasBorder?: boolean;
image: React.ReactNode;
}
export default function StoreSetting() {
const { storeId } = useParams();
const navigate = useNavigate();
const { data } = useSellerData(storeId);
const { theme: initialTheme } = useRootData();
// Check store ownership - this will redirect if user doesn't own the store
const { isLoading: isOwnershipLoading } = useStoreOwnership(storeId);
// State for Instagram sync drawer
const [instagramSyncDrawerOpen, setInstagramSyncDrawerOpen] = useState(false);
const [isCallModalOpen, setIsCallModalOpen] = useState(false);
// Show loading while checking ownership
if (isOwnershipLoading) {
return (
<div className="flex items-center justify-center min-h-[500px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
);
}
// Menu items for store management
const storeMenuItems: MenuItemType[] = [
{
title: "وارد کردن پست های اینستاگرام",
pageName: "instagram-sync",
hasBorder: true,
image: <Instagram size={20} />,
},
{
title: "سفارش‌های دریافتی",
pageName: "seller-orders",
link: `/store/${storeId}/orders`,
image: <ShoppingCart size={20} />,
},
{
title: "پیام های دریافتی",
pageName: "seller-messages",
link: `/store/${storeId}/threads`,
image: <MessageCircle size={20} />,
},
{
title: "داشبورد مالی",
pageName: "financial-dashboard",
link: `/store/${storeId}/financial-dashboard`,
image: <LayoutDashboard size={20} />,
},
{
title: "تعیین نحوه ارسال",
pageName: "shipping-method",
link: `/store/${storeId}/shipping-method`,
image: <Truck size={20} />,
},
{
title: "افزودن محصول جدید",
pageName: "add-product",
link: `/store/add/manual/${storeId}`,
image: <PackagePlus size={20} />,
},
{
title: "لیست محصولات",
pageName: "products-list",
link: `/store/${storeId}/products`,
image: <TableProperties size={20} />,
},
{
title: "ویرایش اطلاعات فروشگاه",
pageName: "edit-store",
link: `/store/${storeId}/edit`,
image: <PenLine size={20} />,
},
{
title: "خروج",
pageName: "logout",
link: `/store/${storeId}/logout`,
hasBorder: true,
image: <LogOut size={20} />,
},
];
const handleBack = () => {
navigate(`/store/${storeId}`);
};
const handleItemClick = (item: MenuItemType) => {
// Special handling for "instagram-sync" item
if (item.pageName === "instagram-sync") {
// Open Instagram sync drawer instead of navigating
setInstagramSyncDrawerOpen(true);
return;
}
// Special handling for "logout" item
if (item.pageName === "logout") {
// Handle logout logic
navigate("/");
return;
}
// Default rendering for other items with Link if they have a link
if (item.link) {
navigate(item.link);
return;
}
};
return (
<div className="flex flex-col bg-WHITE">
{/* Header */}
<div className="lg:hidden">
<HeaderWithSupport title="تنظیمات فروشگاه" onBack={handleBack} />
</div>
<h2 className="hidden lg:block text-[24px] font-extrabold mb-6">
تنظیمات فروشگاه
</h2>
{/* Status + Theme cards */}
<div className="lg:grid lg:grid-cols-3 lg:gap-4 lg:mb-6">
{/* Store Status */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1 w-full">
<div className="flex w-full gap-1 items-center">
<Code size={20} />
<p className="text-R12 lg:text-B16 font-bold">
وضعیت فروشگاه (فعال)
</p>
</div>
<p className="text-R10 lg:text-R12 text-GRAY">
در حالت فعال، فروشگاه برای مخاطبان نمایش داده میشود.
</p>
</div>
<label className="relative inline-flex items-center cursor-pointer">
<input
type="checkbox"
checked={data?.isVerified || false}
className="sr-only peer"
aria-label="وضعیت فروشگاه"
/>
<div className="w-11 h-6 bg-WHITE2 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-WHITE after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-WHITE after:border-GRAY3 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-VITROWN_BLUE border"></div>
</label>
</div>
</div>
{/* Theme Toggle */}
<div className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6">
<div className="flex w-full items-center justify-between">
<div className="flex flex-col gap-1">
<p className="text-R12 lg:text-B16 font-bold">
{initialTheme === "dark" ? "حالت تاریک" : "حالت روشن"}
</p>
<p className="text-R10 lg:text-R12 text-GRAY">
تغییر حالت نمایش به تاریک یا روشن
</p>
</div>
<SunMoonToggle theme={initialTheme} />
</div>
</div>
{/* Push notifications */}
<PushSettingToggle className="p-4 bg-WHITE border-b border-inner-border lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-6" />
</div>
{/* Menu Items */}
<div className="flex flex-col gap-0 w-full bg-WHITE lg:grid lg:grid-cols-3 lg:gap-4">
{storeMenuItems.map((item, index) => {
const isLogout = item.pageName === "logout";
return (
<div
key={index}
className={`flex flex-row gap-2 items-center justify-between px-[16px] active:bg-hover transition cursor-pointer ${
item.hasBorder && "border-t-[2px]"
} h-[55px] w-full lg:h-auto lg:px-0 lg:border-t-0 lg:border lg:border-WHITE3 lg:rounded-2xl lg:p-5 lg:items-center lg:hover:shadow-md lg:transition-shadow ${
isLogout ? "lg:border-RED/40 lg:text-RED" : ""
}`}
onClick={() => handleItemClick(item)}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
handleItemClick(item);
}
}}
>
<div className="flex flex-row gap-2 items-center">
<span
className={`lg:w-11 lg:h-11 lg:rounded-xl lg:flex lg:items-center lg:justify-center ${
isLogout ? "lg:bg-RED/10" : "lg:bg-WHITE2"
}`}
>
{item.image}
</span>
<p className="text-R12 lg:text-B16 font-bold">{item.title}</p>
</div>
<ChevronLeft className="size-4 lg:hidden" />
</div>
);
})}
</div>
{/* Instagram Sync Drawer */}
<InstagramSyncDrawer
setIsCallModalOpen={setIsCallModalOpen}
isOpen={instagramSyncDrawerOpen}
onOpenChange={setInstagramSyncDrawerOpen}
storeId={storeId || ""}
/>
<CallDialog
isCallModalOpen={isCallModalOpen}
setIsCallModalOpen={setIsCallModalOpen}
handleCallConfirm={() => {
window.location.href = `tel:${import.meta.env.VITE_SUPPORT_PHONE}`;
}}
/>
</div>
);
}
export const meta: MetaFunction = () => {
return [
{ title: "تنظیمات فروشگاه - ویترون" },
{
name: "description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
{
name: "keywords",
content:
"تنظیمات فروشگاه، مدیریت تنظیمات فروشگاه، فروشگاه، مشتری، ویترون",
},
{ name: "robots", content: "noindex, follow" },
{ property: "og:title", content: "تنظیمات فروشگاه - ویترون" },
{
property: "og:description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
{ property: "og:type", content: "website" },
{ name: "twitter:card", content: "summary" },
{ name: "twitter:title", content: "تنظیمات فروشگاه - ویترون" },
{
name: "twitter:description",
content:
"مدیریت و مشاهده تمام تنظیمات فروشگاه در فروشگاه شما. فیلتر بر اساس وضعیت و جستجو در تنظیمات فروشگاه.",
},
];
};