Fix Projects nav double-click, shop verify badge, add send-to-users everywhere
- new-project/layout.tsx was hardcoding currentPage="/projects" while the real route is /new-project, so TabNavigation's isExactRoot guard thought it was already on the Projects tab and preventDefault'd the first click with no navigation — now passes the real path (isActive's highlight logic already had a dedicated case for /new-project, so nothing else changes) - resolveShopVerificationTier: shop verify checkmarks always render green regardless of the underlying tier, applied at all 3 shop render sites - Add a "send to users" icon/button next to share on the product detail page, the owner product-manage page, and the shop profile pages (public + owner), reusing/mirroring the existing SendProductModal pattern - New SendShopModal + sharedShop chat-bubble kind (ChatMessageCard via SharedShopBubble) so a whole shop can be sent into a chat, not just one product — backed by the new POST /shops/:id/send endpoint Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,20 +1,25 @@
|
||||
import Header from "@/components/main/Header";
|
||||
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
|
||||
import { generateSeoPageMetadata } from "@/lib/i18n/server";
|
||||
|
||||
|
||||
|
||||
export async function generateMetadata() {
|
||||
|
||||
return generateSeoPageMetadata("newProject", {
|
||||
|
||||
index: false,
|
||||
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
import Header from "@/components/main/Header";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import { generateSeoPageMetadata } from "@/lib/i18n/server";
|
||||
|
||||
export async function generateMetadata() {
|
||||
return generateSeoPageMetadata("newProject", {
|
||||
index: false,
|
||||
});
|
||||
}
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<section className="pb-24">
|
||||
<Header />
|
||||
<LocalePageShell>{children}</LocalePageShell>
|
||||
{/* واقعاً توی /new-project هستیم، نه /projects — قبلاً اینجا "/projects" هاردکد
|
||||
شده بود که باعث میشد تبِ پروژهها فکر کنه از قبل توی روتِ خودشه (isExactRoot)
|
||||
و اولین کلیک روی اون تب preventDefault بشه بدون navigate (کاربر مجبور میشد
|
||||
دوبار بزنه). isActive توی TabNavigation.tsx از قبل شرطِ جداگانه برای
|
||||
"/new-project" داره، پس هایلایتِ تب دستنخورده میمونه */}
|
||||
<TabNavigation currentPage="/new-project" />
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,9 +6,12 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import Header from "@/components/main/Header";
|
||||
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
|
||||
import ShareShopModal from "@/components/shops/ShareShopModal";
|
||||
import SendShopModal from "@/components/shops/SendShopModal";
|
||||
import ShopProductListRow from "@/components/shops/ShopProductListRow";
|
||||
import PageTitle from "@/components/settings/PageTitle";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import VerificationBadge, {
|
||||
resolveShopVerificationTier,
|
||||
} from "@/components/main/VerificationBadge";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -40,6 +43,7 @@ export default function ShopProductsPage() {
|
||||
const [search, setSearch] = useState("");
|
||||
const [onlyInStock, setOnlyInStock] = useState(false);
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
request<{ shop: ShopHeader }>("GET", `/shops/${shopId}`)
|
||||
@@ -114,9 +118,20 @@ export default function ShopProductsPage() {
|
||||
)}
|
||||
>
|
||||
{shop?.name}
|
||||
<VerificationBadge verifyBadge={shop?.owner?.verify_badge} size={13} />
|
||||
<VerificationBadge
|
||||
verifyBadge={resolveShopVerificationTier(shop?.owner?.verify_badge)}
|
||||
size={13}
|
||||
/>
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSendModal(true)}
|
||||
aria-label={t("shops.sendShopTitle")}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="send-2" size={18} tinted />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
@@ -212,6 +227,15 @@ export default function ShopProductsPage() {
|
||||
shopLogo={shop.logo}
|
||||
/>
|
||||
)}
|
||||
{shop && (
|
||||
<SendShopModal
|
||||
open={showSendModal}
|
||||
onClose={() => setShowSendModal(false)}
|
||||
shopId={shop._id}
|
||||
shopName={shop.name}
|
||||
logo={shop.logo}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import Header from "@/components/main/Header";
|
||||
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import BackButton from "@/components/ui/BackButton";
|
||||
import SendProductModal from "@/components/shops/SendProductModal";
|
||||
import DiscountBadge from "@/components/ui/DiscountBadge";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -53,6 +54,7 @@ export default function ProductManagePage() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
const [listing, setListing] = useState<IShopProductListing | null>(null);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||
const [selectedSize, setSelectedSize] = useState<string | null>(null);
|
||||
const [selectedWeight, setSelectedWeight] = useState<string | null>(null);
|
||||
@@ -259,6 +261,14 @@ export default function ProductManagePage() {
|
||||
</span>
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowSendModal(true)}
|
||||
aria-label={t("shops.sendProductTitle")}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="send-2" size={18} tinted />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void shareProduct()}
|
||||
@@ -465,6 +475,14 @@ export default function ProductManagePage() {
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
<TabNavigation currentPage="/settings" />
|
||||
|
||||
{listing ? (
|
||||
<SendProductModal
|
||||
open={showSendModal}
|
||||
onClose={() => setShowSendModal(false)}
|
||||
listing={listing}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,11 +5,13 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import Header from "@/components/main/Header";
|
||||
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
|
||||
import ProductImageLightbox from "@/components/shops/ProductImageLightbox";
|
||||
import SendProductModal from "@/components/shops/SendProductModal";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import BackButton from "@/components/ui/BackButton";
|
||||
import DiscountBadge from "@/components/ui/DiscountBadge";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useRequireAuth } from "@/lib/auth/useRequireAuth";
|
||||
import { copyTextToClipboard } from "@/lib/chat/getCopyableMessageText";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { btnDefault, btnPrimary } from "@/lib/ui/buttonStyles";
|
||||
@@ -52,8 +54,10 @@ export default function ListingDetailClient() {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ listingId: string }>();
|
||||
const { request, loading } = useAxios();
|
||||
const requireAuth = useRequireAuth();
|
||||
|
||||
const [listing, setListing] = useState<IShopProductListing | null>(null);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
const [selectedColor, setSelectedColor] = useState<string | null>(null);
|
||||
const [selectedSize, setSelectedSize] = useState<string | null>(null);
|
||||
const [selectedWeight, setSelectedWeight] = useState<string | null>(null);
|
||||
@@ -308,6 +312,17 @@ export default function ListingDetailClient() {
|
||||
</span>
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!requireAuth()) return;
|
||||
setShowSendModal(true);
|
||||
}}
|
||||
aria-label={t("shops.sendProductTitle")}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="send-2" size={18} tinted />
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void shareProduct()}
|
||||
@@ -573,6 +588,12 @@ export default function ListingDetailClient() {
|
||||
open={showLightbox}
|
||||
onClose={() => setShowLightbox(false)}
|
||||
/>
|
||||
|
||||
<SendProductModal
|
||||
open={showSendModal}
|
||||
onClose={() => setShowSendModal(false)}
|
||||
listing={listing}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,12 @@ import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import ProfileAvatar from "@/components/main/ProfileAvatar";
|
||||
import ExpandableBio from "@/components/profile/ExpandableBio";
|
||||
import ShareShopModal from "@/components/shops/ShareShopModal";
|
||||
import SendShopModal from "@/components/shops/SendShopModal";
|
||||
import ShopContactInfoModal from "@/components/shops/ShopContactInfoModal";
|
||||
import ShopMyOrdersModal from "@/components/shops/ShopMyOrdersModal";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import VerificationBadge, {
|
||||
resolveShopVerificationTier,
|
||||
} from "@/components/main/VerificationBadge";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
@@ -96,6 +99,7 @@ export default function ShopProfileClient() {
|
||||
const [showOrdersModal, setShowOrdersModal] = useState(false);
|
||||
const [showLocationModal, setShowLocationModal] = useState(false);
|
||||
const [showShareModal, setShowShareModal] = useState(false);
|
||||
const [showSendModal, setShowSendModal] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
request<{ shop: PublicShop }>("GET", `/shops/public/${shopId}`)
|
||||
@@ -244,7 +248,10 @@ export default function ShopProfileClient() {
|
||||
)}
|
||||
>
|
||||
{shop.name}
|
||||
<VerificationBadge verifyBadge={shop.owner?.verify_badge} size={16} />
|
||||
<VerificationBadge
|
||||
verifyBadge={resolveShopVerificationTier(shop.owner?.verify_badge)}
|
||||
size={16}
|
||||
/>
|
||||
</h3>
|
||||
{hasDescription ? (
|
||||
<div
|
||||
@@ -297,6 +304,16 @@ export default function ShopProfileClient() {
|
||||
{t("shops.showLocation")}
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (!requireAuth()) return;
|
||||
setShowSendModal(true);
|
||||
}}
|
||||
className={profileActionBtnClass}
|
||||
>
|
||||
{t("shops.sendShopTitle")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
@@ -371,6 +388,13 @@ export default function ShopProfileClient() {
|
||||
shopLogo={shop.logo}
|
||||
responseSchedule={shop.response_schedule}
|
||||
/>
|
||||
<SendShopModal
|
||||
open={showSendModal}
|
||||
onClose={() => setShowSendModal(false)}
|
||||
shopId={shop._id}
|
||||
shopName={shop.name}
|
||||
logo={shop.logo}
|
||||
/>
|
||||
<ShopContactInfoModal
|
||||
open={showContactModal}
|
||||
onClose={() => setShowContactModal(false)}
|
||||
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
} from "@/lib/chat/messageGrouping";
|
||||
import SharedPostBubble, { parseSharedPost } from "./SharedPostBubble";
|
||||
import SharedProductBubble, { parseSharedProduct } from "./SharedProductBubble";
|
||||
import SharedShopBubble, { parseSharedShop } from "./SharedShopBubble";
|
||||
import SharedGiftBubble, { parseSharedGift } from "./SharedGiftBubble";
|
||||
import SharedStoryBubble, {
|
||||
parseSharedStory,
|
||||
@@ -351,6 +352,7 @@ const ChatMessageCard = ({
|
||||
message.forwardedFrom || parseForwarded(message.content || "");
|
||||
const sharedPost = parseSharedPost(message.content || "");
|
||||
const sharedProduct = parseSharedProduct(message.content || "");
|
||||
const sharedShop = parseSharedShop(message.content || "");
|
||||
const sharedGift = parseSharedGift(message.content || "");
|
||||
const sharedStory = parseSharedStory(message.content || "");
|
||||
const storyReactionText = sharedStory
|
||||
@@ -754,6 +756,16 @@ const ChatMessageCard = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sharedShop && (
|
||||
<div
|
||||
className={cn("p-1", isSender ? "chat-bubble-out" : "chat-bubble-in")}
|
||||
style={bubbleStyle}
|
||||
>
|
||||
<SharedShopBubble data={sharedShop} />
|
||||
<TimeBelow />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{sharedGift && (
|
||||
<div
|
||||
className={cn("p-1", isSender ? "chat-bubble-out" : "chat-bubble-in")}
|
||||
|
||||
45
src/components/chat/SharedShopBubble.tsx
Normal file
45
src/components/chat/SharedShopBubble.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface SharedShopPayload {
|
||||
shopId: string;
|
||||
name?: string;
|
||||
logo?: string;
|
||||
url?: string;
|
||||
}
|
||||
|
||||
export function parseSharedShop(content: string): SharedShopPayload | null {
|
||||
if (!content) return null;
|
||||
try {
|
||||
const line = content.split("\n")[0];
|
||||
const j = JSON.parse(line);
|
||||
if (j?.e2ee) return null;
|
||||
return j.sharedShop ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default function SharedShopBubble({ data }: { data: SharedShopPayload }) {
|
||||
const { t } = useTranslation("common");
|
||||
const href = data.url || `/shops/profile/${data.shopId}`;
|
||||
const logo = data.logo ? IMAGE_BASE_URL + data.logo : null;
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
className="flex items-center gap-2 overflow-hidden rounded-2xl border border-white/20 bg-black/10 p-2 min-w-[200px] max-w-[240px]"
|
||||
>
|
||||
<span className="relative h-10 w-10 shrink-0 overflow-hidden rounded-full bg-neutral-800">
|
||||
{logo && <Image src={logo} alt="" fill sizes="40px" className="object-cover" />}
|
||||
</span>
|
||||
<p className="truncate text-[11px] font-semibold opacity-90">
|
||||
{data.name || t("shops.viewShop")}
|
||||
</p>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@@ -34,6 +34,14 @@ export function resolveVerificationStatus(
|
||||
return verifyBadge as VerifyBadgeTier;
|
||||
}
|
||||
|
||||
/** طبق خواستهی صریح کاربر: تیکِ وریفایِ فروشگاه (برخلافِ کاربر که چهار سطح رنگی داره)
|
||||
* همیشه سبز باشه، مستقل از سطح واقعیِ verify_badge — فقط برای رندرِ فروشگاه استفاده بشه */
|
||||
export function resolveShopVerificationTier(
|
||||
verifyBadge?: string | null
|
||||
): "green" | null {
|
||||
return resolveVerificationStatus(verifyBadge) ? "green" : null;
|
||||
}
|
||||
|
||||
const VERIFICATION_ICON_SRC: Record<Exclude<VerifyBadgeTier, "none">, string> = {
|
||||
gray: "/images/icons/verify-gray.svg",
|
||||
blue: "/images/icons/verify-blue.svg",
|
||||
|
||||
205
src/components/shops/SendShopModal.tsx
Normal file
205
src/components/shops/SendShopModal.tsx
Normal file
@@ -0,0 +1,205 @@
|
||||
"use client";
|
||||
|
||||
import { btnPrimary } from "@/lib/ui/buttonStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import Image from "next/image";
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import toast from "react-hot-toast";
|
||||
import IOSSpinner from "@/components/ui/IOSSpinner";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { ChatUserItem } from "./SendProductModal";
|
||||
|
||||
interface SendShopModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
shopId: string;
|
||||
shopName: string;
|
||||
logo?: string | null;
|
||||
}
|
||||
|
||||
// عینِ SendProductModal.tsx، فقط برای خودِ فروشگاه (نه یه کالای خاص) — بکاند
|
||||
// shopController.js: sendShopToUsers
|
||||
export default function SendShopModal({
|
||||
open,
|
||||
onClose,
|
||||
shopId,
|
||||
shopName,
|
||||
logo,
|
||||
}: SendShopModalProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { request } = useAxios();
|
||||
const [users, setUsers] = useState<ChatUserItem[]>([]);
|
||||
const [selected, setSelected] = useState<Set<string>>(new Set());
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sending, setSending] = useState(false);
|
||||
|
||||
const loadUsers = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await request<{ filteredUsersData: ChatUserItem[] }>(
|
||||
"GET",
|
||||
"/messages?limit=100",
|
||||
null,
|
||||
{ noToast: true }
|
||||
);
|
||||
setUsers((res?.filteredUsersData || []).filter((u) => Boolean(u.user_name)));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
loadUsers();
|
||||
setSelected(new Set());
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [open]);
|
||||
|
||||
const toggle = (id: string) => {
|
||||
setSelected((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else if (next.size < 50) next.add(id);
|
||||
else toast.error(t("posts.maxUsers"));
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const send = async () => {
|
||||
if (selected.size === 0) return;
|
||||
setSending(true);
|
||||
try {
|
||||
const res = await request<{ sentCount?: number }>(
|
||||
"POST",
|
||||
`/shops/${shopId}/send`,
|
||||
{ receiverIds: Array.from(selected) },
|
||||
{ noToast: true }
|
||||
);
|
||||
toast.success(
|
||||
t("shops.sentShopToCount", { count: res?.sentCount ?? selected.size })
|
||||
);
|
||||
onClose();
|
||||
setSelected(new Set());
|
||||
} catch {
|
||||
toast.error(t("shops.sendShopError"));
|
||||
} finally {
|
||||
setSending(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
className="glass-modal-overlay fixed inset-0 z-[200] flex items-end justify-center sm:items-center"
|
||||
onClick={onClose}
|
||||
>
|
||||
<motion.div
|
||||
initial={{ y: 40, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
exit={{ y: 40, opacity: 0 }}
|
||||
className="glass-modal-panel w-full max-w-md rounded-t-3xl p-4 sm:rounded-3xl"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
<h3 className="mb-3 text-center text-sm font-bold">
|
||||
{t("shops.sendShopTitle")}
|
||||
</h3>
|
||||
|
||||
<div className="mb-4 flex items-center gap-3 rounded-2xl bg-neutral-100 p-2 dark:bg-neutral-800">
|
||||
{logo && (
|
||||
<div className="relative h-12 w-12 shrink-0 overflow-hidden rounded-full">
|
||||
<Image
|
||||
src={IMAGE_BASE_URL + logo}
|
||||
alt=""
|
||||
fill
|
||||
sizes="48px"
|
||||
className="object-cover"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<p className="line-clamp-2 text-xs text-neutral-600 dark:text-neutral-300">
|
||||
{shopName}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<IOSSpinner />
|
||||
</div>
|
||||
) : users.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-neutral-500">
|
||||
{t("posts.noChatUsers")}
|
||||
</p>
|
||||
) : (
|
||||
<ul className="max-h-[45vh] space-y-1 overflow-y-auto">
|
||||
{users.map((u) => {
|
||||
const name =
|
||||
[u.first_name, u.last_name].filter(Boolean).join(" ") ||
|
||||
u.user_name;
|
||||
const checked = selected.has(u._id);
|
||||
return (
|
||||
<li key={u._id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggle(u._id)}
|
||||
className={`flex w-full items-center gap-3 rounded-2xl px-2 py-2 text-right transition ${
|
||||
checked ? "bg-pink-50 dark:bg-pink-950/30" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="relative h-10 w-10 shrink-0 overflow-hidden rounded-full bg-neutral-200">
|
||||
{u.profile_image ? (
|
||||
<Image
|
||||
src={
|
||||
u.profile_image.startsWith("http")
|
||||
? u.profile_image
|
||||
: IMAGE_BASE_URL + u.profile_image
|
||||
}
|
||||
alt=""
|
||||
fill
|
||||
sizes="40px"
|
||||
className="object-cover"
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<span className="flex-1 text-sm font-medium">{name}</span>
|
||||
<span
|
||||
className={cn(
|
||||
"flex h-5 w-5 items-center justify-center rounded-full border-2",
|
||||
checked ? "btn-modern--selected border-[#FC8EAC]" : "border-neutral-300"
|
||||
)}
|
||||
>
|
||||
{checked ? (
|
||||
<span className="text-[10px] text-white">✓</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
disabled={selected.size === 0 || sending}
|
||||
onClick={send}
|
||||
className={cn(btnPrimary, "mt-4 w-full py-3 text-sm font-bold disabled:opacity-40")}
|
||||
>
|
||||
{sending
|
||||
? t("posts.sending")
|
||||
: t("posts.sendWithCount", { count: selected.size })}
|
||||
</button>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import VerificationBadge, {
|
||||
resolveShopVerificationTier,
|
||||
} from "@/components/main/VerificationBadge";
|
||||
|
||||
type ShopProductAuthorProps = {
|
||||
shopName: string;
|
||||
@@ -28,7 +30,10 @@ export default function ShopProductAuthor({
|
||||
)}
|
||||
<span className="flex min-w-0 items-center gap-1 truncate text-xs font-medium text-neutral-600 dark:text-neutral-300">
|
||||
{shopName}
|
||||
<VerificationBadge verifyBadge={shopVerifyBadge} size={12} />
|
||||
<VerificationBadge
|
||||
verifyBadge={resolveShopVerificationTier(shopVerifyBadge)}
|
||||
size={12}
|
||||
/>
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -2851,6 +2851,7 @@
|
||||
"editShopButton": "Edit shop",
|
||||
"viewPublicProfile": "View shop page",
|
||||
"viewProduct": "View product",
|
||||
"viewShop": "View shop",
|
||||
"myOrdersFromShop": "My orders from this shop",
|
||||
"noOrdersFromShop": "You haven't ordered from this shop yet",
|
||||
"satisfactionLabel": "Product satisfaction",
|
||||
@@ -2869,6 +2870,9 @@
|
||||
"sendProductTitle": "Send product",
|
||||
"sentProductToCount": "Product sent to {{count}} people",
|
||||
"sendProductError": "Failed to send product",
|
||||
"sendShopTitle": "Send shop",
|
||||
"sentShopToCount": "Shop sent to {{count}} people",
|
||||
"sendShopError": "Failed to send shop",
|
||||
"noChatMessagesYet": "No messages yet",
|
||||
"performance": {
|
||||
"totalRevenue": "Total revenue (Toman)",
|
||||
|
||||
@@ -2856,6 +2856,7 @@
|
||||
"editShopButton": "ویرایش فروشگاه",
|
||||
"viewPublicProfile": "نمایش صفحه فروشگاه",
|
||||
"viewProduct": "مشاهده کالا",
|
||||
"viewShop": "مشاهده فروشگاه",
|
||||
"myOrdersFromShop": "پیگیری سفارشات من",
|
||||
"noOrdersFromShop": "هنوز سفارشی از این فروشگاه ثبت نکردهاید",
|
||||
"satisfactionLabel": "رضایت از کالاها",
|
||||
@@ -2874,6 +2875,9 @@
|
||||
"sendProductTitle": "ارسال کالا",
|
||||
"sentProductToCount": "کالا به {{count}} نفر ارسال شد",
|
||||
"sendProductError": "خطا در ارسال کالا",
|
||||
"sendShopTitle": "ارسال فروشگاه",
|
||||
"sentShopToCount": "فروشگاه به {{count}} نفر ارسال شد",
|
||||
"sendShopError": "خطا در ارسال فروشگاه",
|
||||
"noChatMessagesYet": "هنوز پیامی رد و بدل نشده است",
|
||||
"performance": {
|
||||
"totalRevenue": "درآمد کل (تومان)",
|
||||
|
||||
Reference in New Issue
Block a user