+
+
{listing.title}
+ {discountPercent != null && (
+
+
+
+
+ {t("shops.discountPercentBadge", { percent: discountPercent })}
+
+
+ )}
- )}
- {listing.description && (
-
- {listing.description}
-
- )}
+ {images.length > 0 && (
+
+
+ {images.map((img) => (
+
+ ))}
+
+ {images.length > 1 && (
+
+ {activeImageIndex + 1}/{images.length}
+
+ )}
+
+ )}
- {listing.variants.length > 1 && (
-
-
{t("shops.variantsTitle")}
-
- {listing.variants.map((v) => (
+
{t("shops.specsTitle")}
+
+ {categoryLine && (
+
+ {categoryLine}
+
+ )}
+
+ {colors.length > 0 && (
+
+ {colors.map((color) => (
))}
-
-
- )}
+
+ )}
- {selectedVariant && (
-
- {t("shops.priceLabel", {
- amount: (
- selectedVariant.discount_price || selectedVariant.price
- ).toLocaleString(),
- })}
-
- )}
+ {sizes.length > 0 && (
+
+ {sizes.map((size) => (
+
+ ))}
+
+ )}
-
- {t("shops.quantityLabel")}
-
- {quantity}
-
-
+ {weights.length > 0 && (
+
+ {weights.map((weight) => (
+
+ ))}
+
+ )}
- {shop && shop.shipping_methods.filter((m) => m.enabled).length > 0 && (
-
-
{t("shops.shippingTitle")}
-
- {shop.shipping_methods
- .filter((m) => m.enabled)
- .map((m) => (
-
- ))}
-
-
- )}
+ {enabledShippingMethods.length > 0 && (
+
+ {enabledShippingMethods.map((m) => (
+
+ ))}
+
+ )}
-
- {t("shops.buyNow")}
-
-
-
-
{t("shops.addressRequiredHint")}
-
router.push("/settings/edit/location")}
- >
- {t("shops.goToLocationSettings")}
-
+
+
+ {quantity}
+
+
+ {matchedVariant && (
+
+ {matchedVariant.discount_price ? (
+
+
+ {matchedVariant.price.toLocaleString()} {t("settings.toman")}
+
+
+ {matchedVariant.discount_price.toLocaleString()} {t("settings.toman")}
+
+
+ ) : (
+
+ {matchedVariant.price.toLocaleString()} {t("settings.toman")}
+
+ )}
+
+ )}
- )}
-
-
+
+
+
+
+
+
+
+
+
+ {showAddressModal && (
+
+
+
{t("shops.addressRequiredHint")}
+
+
+
+
+ )}
+
+
setShowLightbox(false)}
+ />
+ >
);
}
diff --git a/src/app/shops/new/category/page.tsx b/src/app/shops/new/category/page.tsx
deleted file mode 100644
index 25bca7a..0000000
--- a/src/app/shops/new/category/page.tsx
+++ /dev/null
@@ -1,104 +0,0 @@
-"use client";
-
-import AuthPageLayout, {
- AuthFormFooter,
- AuthPageContent,
-} from "@/components/auth/AuthPageLayout";
-import AuthNextButton from "@/components/auth/AuthNextButton";
-import LocalePageShell from "@/components/i18n/LocalePageShell";
-import ShopCategoryPicker from "@/components/shops/ShopCategoryPicker";
-import useAxios from "@/hooks/useAxios";
-import { useShopWizardId } from "@/hooks/useShopWizardId";
-import { IShopCategory } from "@/types/types";
-import { useRouter } from "next/navigation";
-import { useEffect, useState } from "react";
-import toast from "react-hot-toast";
-import { useTranslation } from "react-i18next";
-
-function ShopCategoryPage() {
- const { t } = useTranslation("common");
- const router = useRouter();
- const { request, loading } = useAxios();
- const shopId = useShopWizardId();
-
- const [categoryList, setCategoryList] = useState(null);
- const [category, setCategory] = useState("");
- const [subCategory, setSubCategory] = useState([]);
- const [displaySubCategory, setDisplaySubCategory] = useState(
- null
- );
-
- useEffect(() => {
- request<{ categories: IShopCategory[] }>("GET", "/shop-categories")
- .then((res) => setCategoryList(res?.categories || []))
- .catch(() => setCategoryList([]));
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, []);
-
- const handleCategorySelect = (value: string) => {
- setCategory(value);
- setSubCategory([]);
- setDisplaySubCategory(null);
- };
-
- const handleSubToggle = (value: string) => {
- setSubCategory((prev) =>
- prev.includes(value) ? prev.filter((v) => v !== value) : [...prev, value]
- );
- if (displaySubCategory === value) setDisplaySubCategory(null);
- };
-
- const handleSubmit = async () => {
- if (!shopId) return;
- if (!category || subCategory.length === 0 || !displaySubCategory) {
- toast.error(t("shops.categoryRequired"));
- return;
- }
- try {
- await request("PATCH", `/shops/${shopId}/category`, {
- category,
- sub_category: subCategory.join(","),
- display_sub_category: displaySubCategory,
- });
- router.push("/shops/new/shipping");
- } catch (err: unknown) {
- const message =
- (err as { response?: { data?: { message?: string } } })?.response
- ?.data?.message || t("shops.unknownError");
- toast.error(message);
- }
- };
-
- if (!shopId) return null;
-
- return (
-
-
-
- {t("shops.categoryTitle")}
-
-
-
-
- {t("shops.saveAndContinue")}
-
-
-
-
- );
-}
-
-export default ShopCategoryPage;
diff --git a/src/app/shops/new/contact/page.tsx b/src/app/shops/new/contact/page.tsx
index 1881346..2bc35c1 100644
--- a/src/app/shops/new/contact/page.tsx
+++ b/src/app/shops/new/contact/page.tsx
@@ -7,10 +7,13 @@ import AuthPageLayout, {
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
+import ToggleSwitch from "@/components/shops/ToggleSwitch";
+import TimeSelect from "@/components/shops/TimeSelect";
import useAxios from "@/hooks/useAxios";
import { useShopWizardId } from "@/hooks/useShopWizardId";
+import Image from "next/image";
import { useRouter } from "next/navigation";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -24,12 +27,11 @@ function ShopContactPage() {
const shopId = useShopWizardId();
const [contact, setContact] = useState({
- phone: "",
+ landline: "",
mobile: "",
telegram: "",
whatsapp: "",
instagram: "",
- landline: "",
});
const [enabledDays, setEnabledDays] = useState>({
@@ -57,6 +59,42 @@ function ShopContactPage() {
setEnabledDays((prev) => ({ ...prev, [day]: !prev[day] }));
};
+ useEffect(() => {
+ if (!shopId) return;
+ request<{
+ shop: {
+ contact?: Record;
+ response_schedule?: { day: Day; start_time: string | null; end_time: string | null }[];
+ };
+ }>("GET", `/shops/${shopId}`).then((res) => {
+ const shop = res?.shop;
+ if (!shop) return;
+ if (shop.contact) {
+ setContact((prev) => ({
+ landline: shop.contact?.landline || prev.landline,
+ mobile: shop.contact?.mobile || prev.mobile,
+ telegram: shop.contact?.telegram || prev.telegram,
+ whatsapp: shop.contact?.whatsapp || prev.whatsapp,
+ instagram: shop.contact?.instagram || prev.instagram,
+ }));
+ }
+ if (shop.response_schedule?.length) {
+ const nextEnabled = { ...enabledDays };
+ const nextTimes = { ...times };
+ shop.response_schedule.forEach((item) => {
+ nextEnabled[item.day] = true;
+ nextTimes[item.day] = {
+ start: item.start_time || "",
+ end: item.end_time || "",
+ };
+ });
+ setEnabledDays(nextEnabled);
+ setTimes(nextTimes);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shopId]);
+
const handleSubmit = async () => {
if (!shopId) return;
@@ -90,6 +128,42 @@ function ShopContactPage() {
if (!shopId) return null;
+ const contactFields: {
+ key: keyof typeof contact;
+ icon: string;
+ placeholder: string;
+ maxLength?: number;
+ }[] = [
+ {
+ key: "landline",
+ icon: "/images/icons/vuesax/bold/call.svg",
+ placeholder: t("shops.landlinePlaceholder"),
+ maxLength: 11,
+ },
+ {
+ key: "mobile",
+ icon: "/images/icons/vuesax/bold/mobile.svg",
+ placeholder: t("billboards.form.mobile"),
+ maxLength: 11,
+ },
+ {
+ key: "telegram",
+ icon: "/images/icons/telegram.svg",
+ placeholder: t("billboards.form.telegram"),
+ },
+ {
+ key: "whatsapp",
+ icon: "/images/icons/vuesax/bold/whatsapp.svg",
+ placeholder: t("billboards.form.whatsapp"),
+ maxLength: 11,
+ },
+ {
+ key: "instagram",
+ icon: "/images/icons/vuesax/bold/instagram.svg",
+ placeholder: t("billboards.form.instagram"),
+ },
+ ];
+
return (
@@ -97,102 +171,80 @@ function ShopContactPage() {
{t("shops.contactTitle")}
-
- setContact((prev) => ({ ...prev, landline: e.target.value }))
- }
- />
-
- setContact((prev) => ({ ...prev, phone: e.target.value }))
- }
- />
-
- setContact((prev) => ({ ...prev, mobile: e.target.value }))
- }
- />
-
- setContact((prev) => ({ ...prev, telegram: e.target.value }))
- }
- />
-
- setContact((prev) => ({ ...prev, whatsapp: e.target.value }))
- }
- />
-
- setContact((prev) => ({ ...prev, instagram: e.target.value }))
- }
- />
-
-
- {t("shops.responseScheduleTitle")}
-
- {DAYS.map((day) => (
-
-
toggleDay(day)}
- />
-
- {t(`shops.days.${day}`)}
+ {contactFields.map((field) => (
+
+
+
- {enabledDays[day] && (
- <>
-
- setTimes((prev) => ({
- ...prev,
- [day]: { ...prev[day], start: e.target.value },
- }))
- }
- />
- -
-
- setTimes((prev) => ({
- ...prev,
- [day]: { ...prev[day], end: e.target.value },
- }))
- }
- />
- >
- )}
+
+ setContact((prev) => ({ ...prev, [field.key]: e.target.value }))
+ }
+ />
))}
+
+
+ {t("shops.responseScheduleTitle")}
+
+
+ {DAYS.map((day) => (
+
+
+
+
+ {t(`shops.days.${day}`)}
+
+
+
+ {enabledDays[day] && (
+
+
+ setTimes((prev) => ({
+ ...prev,
+ [day]: { ...prev[day], end: value },
+ }))
+ }
+ />
+ -
+
+ setTimes((prev) => ({
+ ...prev,
+ [day]: { ...prev[day], start: value },
+ }))
+ }
+ />
+
+ )}
+
+
toggleDay(day)}
+ ariaLabel={t(`shops.days.${day}`)}
+ />
+
+ ))}
+
diff --git a/src/app/shops/new/location/page.tsx b/src/app/shops/new/location/page.tsx
index fab0a1f..e674606 100644
--- a/src/app/shops/new/location/page.tsx
+++ b/src/app/shops/new/location/page.tsx
@@ -43,6 +43,38 @@ function ShopLocationPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
+ useEffect(() => {
+ if (!shopId) return;
+ request<{
+ shop: {
+ has_physical_location?: boolean;
+ province?: IProvince | null;
+ city?: ICity | null;
+ neighbourhood?: string | null;
+ address?: string | null;
+ lat?: string | null;
+ lng?: string | null;
+ };
+ }>("GET", `/shops/${shopId}`).then((res) => {
+ const shop = res?.shop;
+ if (!shop) return;
+ setHasPhysicalLocation(shop.has_physical_location ?? true);
+ if (shop.province) {
+ setProvinceId(String(shop.province.id));
+ request<{ cities: ICity[] }>("GET", `/cities/${shop.province.id}`)
+ .then((r) => setCities(r?.cities || []))
+ .catch(() => setCities([]));
+ }
+ if (shop.city) setCityId(String(shop.city.id));
+ if (shop.neighbourhood) setNeighbourhood(shop.neighbourhood);
+ if (shop.address) setAddress(shop.address);
+ if (shop.lat && shop.lng) {
+ setMarker({ lat: Number(shop.lat), lng: Number(shop.lng) });
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shopId]);
+
const handleProvinceChange = (e: React.ChangeEvent) => {
const id = e.target.value;
setProvinceId(id);
@@ -62,6 +94,15 @@ function ShopLocationPage() {
const province = provinces?.find((p) => String(p.id) === provinceId) || null;
const city = cities?.find((c) => String(c.id) === cityId) || null;
+ if (!province) {
+ toast.error(t("shops.provinceRequired"));
+ return;
+ }
+ if (!city) {
+ toast.error(t("shops.cityRequired"));
+ return;
+ }
+
try {
await request("PATCH", `/shops/${shopId}/location`, {
has_physical_location: hasPhysicalLocation,
@@ -100,9 +141,7 @@ function ShopLocationPage() {
{t("shops.noPhysicalShop")}
- {hasPhysicalLocation && (
- <>
-
+
@@ -167,8 +206,6 @@ function ShopLocationPage() {
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
- >
- )}
diff --git a/src/app/shops/new/logo/page.tsx b/src/app/shops/new/logo/page.tsx
index cc49cf7..6c7a2ce 100644
--- a/src/app/shops/new/logo/page.tsx
+++ b/src/app/shops/new/logo/page.tsx
@@ -6,11 +6,12 @@ import AuthPageLayout, {
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
+import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import { useShopWizardId } from "@/hooks/useShopWizardId";
import Image from "next/image";
import { useRouter } from "next/navigation";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -22,6 +23,16 @@ function ShopLogoPage() {
const [logoPreview, setLogoPreview] = useState(null);
const [logoFile, setLogoFile] = useState(null);
+ useEffect(() => {
+ if (!shopId) return;
+ request<{ shop: { logo?: string | null } }>("GET", `/shops/${shopId}`).then((res) => {
+ if (res?.shop?.logo) {
+ setLogoPreview(IMAGE_BASE_URL + res.shop.logo);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shopId]);
+
const selectImage = (event: React.ChangeEvent) => {
const file = event.target.files?.[0];
if (!file) return;
@@ -31,7 +42,7 @@ function ShopLogoPage() {
reader.readAsDataURL(file);
};
- const goNext = () => router.push("/shops/new/category");
+ const goNext = () => router.push("/shops/new/shipping");
const handleUpload = async () => {
if (!logoFile || !shopId) {
@@ -41,9 +52,7 @@ function ShopLogoPage() {
const formData = new FormData();
formData.append("logo", logoFile);
try {
- await request("PATCH", `/shops/${shopId}/logo`, formData, {
- headers: { "Content-Type": "multipart/form-data" },
- });
+ await request("PATCH", `/shops/${shopId}/logo`, formData);
goNext();
} catch (err: unknown) {
const message =
diff --git a/src/app/shops/new/name/page.tsx b/src/app/shops/new/name/page.tsx
index 33b87e7..88e4833 100644
--- a/src/app/shops/new/name/page.tsx
+++ b/src/app/shops/new/name/page.tsx
@@ -4,35 +4,49 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
-import AuthInput from "@/components/auth/AuthInput";
import AuthNextButton from "@/components/auth/AuthNextButton";
+import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import useAxios from "@/hooks/useAxios";
+import { getShopWizardId, saveShopWizardId } from "@/hooks/useShopWizardId";
import { useFormik } from "formik";
import * as yup from "yup";
-import { useRouter } from "next/navigation";
+import { useRouter, useSearchParams } from "next/navigation";
+import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
function ShopNamePage() {
const { t } = useTranslation("common");
const router = useRouter();
+ const searchParams = useSearchParams();
const { request, loading } = useAxios();
+ const isEditMode = searchParams.get("edit") === "1";
+ const [editShopId] = useState(() => (isEditMode ? getShopWizardId() : null));
const formik = useFormik({
- initialValues: { name: "" },
+ initialValues: { name: "", description: "" },
validationSchema: yup.object({
name: yup.string().trim().required(t("shops.nameRequired")),
}),
onSubmit: async (values) => {
try {
+ if (editShopId) {
+ await request("PATCH", `/shops/${editShopId}/basic`, {
+ name: values.name.trim(),
+ description: values.description.trim() || undefined,
+ });
+ router.push("/shops/new/logo");
+ return;
+ }
+
const response = await request<{ shopId: string }>(
"POST",
"/shops/draft",
- { name: values.name.trim() }
+ { name: values.name.trim(), description: values.description.trim() || undefined }
);
- if (typeof window !== "undefined" && response?.shopId) {
- localStorage.setItem("shop_wizard_shop_id", String(response.shopId));
+ if (response?.shopId) {
+ saveShopWizardId(String(response.shopId));
}
router.push("/shops/new/logo");
} catch (err: unknown) {
@@ -44,6 +58,21 @@ function ShopNamePage() {
},
});
+ useEffect(() => {
+ if (!editShopId) return;
+ request<{ shop: { name: string; description?: string | null } }>(
+ "GET",
+ `/shops/${editShopId}`
+ ).then((res) => {
+ if (!res?.shop) return;
+ formik.setValues({
+ name: res.shop.name || "",
+ description: res.shop.description || "",
+ });
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [editShopId]);
+
return (
@@ -53,20 +82,32 @@ function ShopNamePage() {
>
{t("shops.nameTitle")}
-
{formik.touched.name && formik.errors.name && (
{formik.errors.name}
)}
+
+
= {
+ post: "send",
+ tipax: "truck-fast",
+ chapar: "truck-tick",
+ intercity_freight: "truck",
+ courier: "car",
+ own_vehicle: "smart-car",
+};
+
+const CONTACT_CHANNELS: {
+ key: "mobile" | "landline" | "telegram" | "whatsapp" | "instagram";
+ icon: string;
+ color: string;
+}[] = [
+ { key: "mobile", icon: "/images/icons/vuesax/bold/mobile.svg", color: "bg-green-500" },
+ { key: "landline", icon: "/images/icons/vuesax/bold/call.svg", color: "bg-orange-500" },
+ { key: "telegram", icon: "/images/icons/telegram.svg", color: "bg-sky-500" },
+ { key: "whatsapp", icon: "/images/icons/vuesax/bold/whatsapp.svg", color: "bg-emerald-600" },
+ {
+ key: "instagram",
+ icon: "/images/icons/vuesax/bold/instagram.svg",
+ color: "bg-gradient-to-tr from-yellow-400 via-pink-500 to-purple-600",
+ },
+];
+
type ShopPreview = {
name: string;
logo?: string | null;
- category?: string | null;
- sub_category?: string | null;
shipping_methods?: { method: string; cost: number }[];
- address?: string | null;
+ cod_available?: boolean;
+ same_day_available?: boolean;
+ free_shipping_threshold?: number | null;
+ estimated_delivery_text?: string | null;
has_physical_location?: boolean;
- contact?: { mobile?: string | null; telegram?: string | null } | null;
+ province?: { name?: string } | null;
+ city?: { name?: string } | null;
+ neighbourhood?: string | null;
+ address?: string | null;
+ lat?: string | null;
+ lng?: string | null;
+ contact?: {
+ mobile?: string | null;
+ landline?: string | null;
+ telegram?: string | null;
+ whatsapp?: string | null;
+ instagram?: string | null;
+ } | null;
+ response_schedule?: { day: string; start_time: string | null; end_time: string | null }[];
};
function ShopPreviewPage() {
@@ -43,12 +83,12 @@ function ShopPreviewPage() {
const handleSubmit = async () => {
if (!shopId) return;
try {
- await request("POST", `/shops/${shopId}/submit`);
+ await request("POST", `/shops/${shopId}/submit`, {});
if (typeof window !== "undefined") {
localStorage.removeItem("shop_wizard_shop_id");
}
toast.success(t("shops.submitSuccess"));
- router.push("/settings/shop");
+ router.push("/settings/shop/list");
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response
@@ -59,55 +99,179 @@ function ShopPreviewPage() {
if (!shopId || !shop) return null;
+ const orderInfoRows = [
+ shop.free_shipping_threshold
+ ? {
+ key: "minOrder",
+ label: t("shops.previewMinOrder"),
+ value: `${shop.free_shipping_threshold.toLocaleString()} ${t("settings.toman")}`,
+ }
+ : null,
+ shop.estimated_delivery_text
+ ? { key: "delivery", label: t("shops.previewDeliveryTime"), value: shop.estimated_delivery_text }
+ : null,
+ shop.cod_available
+ ? { key: "cod", label: t("shops.codAvailable"), value: t("shops.previewYes") }
+ : null,
+ shop.same_day_available
+ ? { key: "sameDay", label: t("shops.sameDayAvailable"), value: t("shops.previewYes") }
+ : null,
+ ].filter(Boolean) as { key: string; label: string; value: string }[];
+
+ const hasLocationInfo =
+ shop.has_physical_location &&
+ (shop.address || shop.neighbourhood || shop.city?.name || shop.province?.name);
+ const hasMap = shop.has_physical_location && shop.lat && shop.lng;
+
return (
-
-
- {t("shops.previewTitle")}
+
+
+
{t("shops.previewTitle")}
-
- {shop.logo && (
- // eslint-disable-next-line @next/next/no-img-element
-

- )}
-
{shop.name}
- {shop.category && (
-
- {shop.category}
- {shop.sub_category ? ` / ${shop.sub_category}` : ""}
-
- )}
- {shop.shipping_methods && shop.shipping_methods.length > 0 && (
-
-
{t("shops.shippingTitle")}
+ {shop.logo && (
+ // eslint-disable-next-line @next/next/no-img-element
+

+ )}
+
+
{shop.name}
+
+ {shop.shipping_methods && shop.shipping_methods.length > 0 && (
+
+
{t("shops.shippingTitle")}
+
{shop.shipping_methods.map((m) => (
-
- {t(`shops.shippingMethods.${m.method}`)} —{" "}
- {m.cost.toLocaleString()} {t("settings.toman")}
-
+
+
+
+
+
+
+ {t(`shops.shippingMethods.${m.method}`)}
+
+
+
+ {m.cost.toLocaleString()} {t("settings.toman")}
+
+
))}
+
+ )}
+
+ {orderInfoRows.length > 0 && (
+
+ {orderInfoRows.map((row) => (
+
+ {row.label}
+ {row.value}
+
+ ))}
+
+ )}
+
+ {shop.contact &&
+ CONTACT_CHANNELS.some((ch) => shop.contact?.[ch.key]) && (
+
+
{t("shops.contactTitle")}
+
+ {CONTACT_CHANNELS.filter((ch) => shop.contact?.[ch.key]).map((ch) => (
+
+
+ {/* eslint-disable-next-line @next/next/no-img-element */}
+
+
+
+ {shop.contact?.[ch.key]}
+
+
+ ))}
+
+
)}
- {shop.has_physical_location && shop.address && (
-
{shop.address}
- )}
-
-
-
- 0 && (
+
+
{t("shops.responseScheduleTitle")}
+
+ {shop.response_schedule.map((item) => (
+
+
+ {item.start_time || "--:--"} - {item.end_time || "--:--"}
+
+ {t(`shops.days.${item.day}`)}
+
+ ))}
+
+
+ )}
+
+ {hasLocationInfo && (
+
+ {(shop.city?.name || shop.neighbourhood) && (
+
+ {shop.city?.name && (
+
+ {shop.city.name}
+
+ )}
+ {shop.neighbourhood && (
+
+ {shop.neighbourhood}
+
+ )}
+
+ )}
+ {shop.address && (
+
+ {shop.address}
+
+ )}
+ {hasMap && (
+
+
+
+ )}
+
+ )}
+
+
- {t("shops.submitShop")}
-
-
-
+ {loading ? t("common.loading") : t("shops.submitShop")}
+
+
+
);
}
diff --git a/src/app/shops/new/shipping/page.tsx b/src/app/shops/new/shipping/page.tsx
index b1f9b73..dd02865 100644
--- a/src/app/shops/new/shipping/page.tsx
+++ b/src/app/shops/new/shipping/page.tsx
@@ -7,14 +7,16 @@ import AuthPageLayout, {
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
+import ToggleSwitch from "@/components/shops/ToggleSwitch";
+import BoldIcon from "@/components/ui/BoldIcon";
import useAxios from "@/hooks/useAxios";
import { useShopWizardId } from "@/hooks/useShopWizardId";
import { useRouter } from "next/navigation";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
-const SHIPPING_METHODS = [
+const DELIVERY_METHODS = [
"post",
"tipax",
"chapar",
@@ -23,7 +25,16 @@ const SHIPPING_METHODS = [
"own_vehicle",
] as const;
-type ShippingMethod = (typeof SHIPPING_METHODS)[number];
+const METHOD_ICON: Record<(typeof DELIVERY_METHODS)[number], string> = {
+ post: "send",
+ tipax: "truck-fast",
+ chapar: "truck-tick",
+ intercity_freight: "truck",
+ courier: "car",
+ own_vehicle: "smart-car",
+};
+
+type DeliveryMethod = (typeof DELIVERY_METHODS)[number];
function ShopShippingPage() {
const { t } = useTranslation("common");
@@ -31,9 +42,7 @@ function ShopShippingPage() {
const { request, loading } = useAxios();
const shopId = useShopWizardId();
- const [enabledMethods, setEnabledMethods] = useState<
- Record
- >({
+ const [enabledMethods, setEnabledMethods] = useState>({
post: false,
tipax: false,
chapar: false,
@@ -41,7 +50,7 @@ function ShopShippingPage() {
courier: false,
own_vehicle: false,
});
- const [costs, setCosts] = useState>({
+ const [costs, setCosts] = useState>({
post: "",
tipax: "",
chapar: "",
@@ -54,14 +63,49 @@ function ShopShippingPage() {
const [freeShippingThreshold, setFreeShippingThreshold] = useState("");
const [estimatedDeliveryText, setEstimatedDeliveryText] = useState("");
- const toggleMethod = (method: ShippingMethod) => {
+ useEffect(() => {
+ if (!shopId) return;
+ request<{
+ shop: {
+ shipping_methods?: { method: DeliveryMethod; cost: number }[];
+ cod_available?: boolean;
+ same_day_available?: boolean;
+ free_shipping_threshold?: number | null;
+ estimated_delivery_text?: string | null;
+ };
+ }>("GET", `/shops/${shopId}`).then((res) => {
+ const shop = res?.shop;
+ if (!shop) return;
+ if (shop.shipping_methods?.length) {
+ const nextEnabled = { ...enabledMethods };
+ const nextCosts = { ...costs };
+ shop.shipping_methods.forEach((m) => {
+ nextEnabled[m.method] = true;
+ nextCosts[m.method] = String(m.cost || "");
+ });
+ setEnabledMethods(nextEnabled);
+ setCosts(nextCosts);
+ }
+ setCodAvailable(Boolean(shop.cod_available));
+ setSameDayAvailable(Boolean(shop.same_day_available));
+ if (shop.free_shipping_threshold) {
+ setFreeShippingThreshold(String(shop.free_shipping_threshold));
+ }
+ if (shop.estimated_delivery_text) {
+ setEstimatedDeliveryText(shop.estimated_delivery_text);
+ }
+ });
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shopId]);
+
+ const toggleMethod = (method: DeliveryMethod) => {
setEnabledMethods((prev) => ({ ...prev, [method]: !prev[method] }));
};
const handleSubmit = async () => {
if (!shopId) return;
- const shipping_methods = SHIPPING_METHODS.filter(
+ const shipping_methods = DELIVERY_METHODS.filter(
(method) => enabledMethods[method]
).map((method) => ({
method,
@@ -102,66 +146,92 @@ function ShopShippingPage() {
{t("shops.shippingTitle")}
- {SHIPPING_METHODS.map((method) => (
-
-
toggleMethod(method)}
- />
-
- {t(`shops.shippingMethods.${method}`)}
-
- {enabledMethods[method] && (
-
- setCosts((prev) => ({ ...prev, [method]: e.target.value }))
- }
- />
- )}
+
+
+
+ {t("shops.activeShippingMethods")}
+
+
+ {DELIVERY_METHODS.map((method) => (
+
+
+
+
+
+
+
+ {t(`shops.shippingMethods.${method}`)}
+
+
+
toggleMethod(method)}
+ ariaLabel={t(`shops.shippingMethods.${method}`)}
+ />
+
+ {enabledMethods[method] && (
+
+
+ setCosts((prev) => ({ ...prev, [method]: e.target.value }))
+ }
+ />
+
+ {t("settings.toman")}
+
+
+ )}
+
+ ))}
- ))}
+
-
-
+
+
+ {t("shops.supplementarySettings")}
+
+
+
+ {t("shops.codAvailable")}
+
+
+
+ {t("shops.sameDayAvailable")}
+
+
-
setFreeShippingThreshold(e.target.value)}
- />
- setEstimatedDeliveryText(e.target.value)}
- />
+ setFreeShippingThreshold(e.target.value)}
+ />
+ setEstimatedDeliveryText(e.target.value)}
+ />
+
+
diff --git a/src/app/shops/orders/[orderId]/page.tsx b/src/app/shops/orders/[orderId]/page.tsx
index 173c88d..5836851 100644
--- a/src/app/shops/orders/[orderId]/page.tsx
+++ b/src/app/shops/orders/[orderId]/page.tsx
@@ -2,10 +2,14 @@
import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
+import Header from "@/components/main/Header";
+import TabNavigation from "@/components/TabNavigation";
import PageTitle from "@/components/settings/PageTitle";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
-import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
+import BuyerInfoModal from "@/components/shops/orders/BuyerInfoModal";
+import ShopInfoModal from "@/components/shops/orders/ShopInfoModal";
+import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import { useUser } from "@/hooks/useUser";
import { useParams } from "next/navigation";
@@ -16,9 +20,23 @@ import { useTranslation } from "react-i18next";
const TRACKING_REQUIRED_METHODS = ["post", "tipax", "chapar", "intercity_freight"];
const SELLER_STATUSES = ["on_hold", "processing", "shipped", "ready_for_pickup"];
+/** These carriers have a public tracking page we can deep-link to. */
+const BOLD_TRACKING_METHODS = ["post", "tipax", "chapar"];
+
+function trackingUrl(method: string, code: string): string | null {
+ if (method === "post") return `https://tracking.post.ir/?id=${code}`;
+ if (method === "tipax") return `https://tipaxco.com/tracking?code=${code}`;
+ if (method === "chapar") return `https://www.chapar.co/tracking?code=${code}`;
+ return null;
+}
+
+type OrderReport = { _id: string; description: string; status: string; createdAt: string };
+type ReturnRequest = { _id: string; reason: string; status: string; createdAt: string };
+type ShopRating = { _id: string; rating: number; comment?: string | null };
type OrderDetail = {
_id: string;
+ order_number: string;
quantity: number;
unit_price: number;
total_amount: number;
@@ -33,12 +51,26 @@ type OrderDetail = {
buyerAddressSnapshot?: {
first_name?: string | null;
last_name?: string | null;
+ mobile?: string | null;
address?: string | null;
province?: { name?: string } | null;
city?: { name?: string } | null;
+ lat?: string | null;
+ lng?: string | null;
} | null;
listing: { title: string; images: string[]; primaryImageIndex: number };
- shop: { _id: string; name: string; owner: string };
+ shop: {
+ _id: string;
+ name: string;
+ owner: string;
+ estimated_delivery_text?: string | null;
+ has_physical_location?: boolean;
+ province?: { name?: string } | null;
+ city?: { name?: string } | null;
+ address?: string | null;
+ lat?: string | null;
+ lng?: string | null;
+ };
};
export default function OrderDetailPage() {
@@ -48,23 +80,34 @@ export default function OrderDetailPage() {
const user = useUser();
const [order, setOrder] = useState(null);
+ const [report, setReport] = useState(null);
+ const [returnRequest, setReturnRequest] = useState(null);
+ const [shopRating, setShopRating] = useState(null);
const [trackingCode, setTrackingCode] = useState("");
- const [printMode, setPrintMode] = useState<"label-a5" | "label-a6" | "invoice" | null>(
- null
- );
+ const [printMode, setPrintMode] = useState<"invoice" | null>(null);
const [showRating, setShowRating] = useState(false);
- const [ratingScore, setRatingScore] = useState(5);
+ const [ratingScore, setRatingScore] = useState(0);
const [ratingComment, setRatingComment] = useState("");
const [showReport, setShowReport] = useState(false);
const [reportText, setReportText] = useState("");
const [showReturn, setShowReturn] = useState(false);
const [returnReason, setReturnReason] = useState("");
+ const [showBuyerInfo, setShowBuyerInfo] = useState(false);
+ const [showShopInfo, setShowShopInfo] = useState(false);
const loadOrder = () => {
- request<{ order: OrderDetail }>("GET", `/orders/${params.orderId}`)
+ request<{
+ order: OrderDetail;
+ report?: OrderReport | null;
+ returnRequest?: ReturnRequest | null;
+ shopRating?: ShopRating | null;
+ }>("GET", `/orders/${params.orderId}`)
.then((res) => {
setOrder(res?.order || null);
setTrackingCode(res?.order?.tracking_code || "");
+ setReport(res?.report || null);
+ setReturnRequest(res?.returnRequest || null);
+ setShopRating(res?.shopRating || null);
})
.catch(() => setOrder(null));
};
@@ -107,7 +150,7 @@ export default function OrderDetailPage() {
};
const handleSubmitRating = async () => {
- if (!order) return;
+ if (!order || ratingScore < 1) return;
try {
await request("POST", `/orders/${order._id}/rate`, {
rating: ratingScore,
@@ -116,6 +159,7 @@ export default function OrderDetailPage() {
toast.success(t("shops.ratingSubmitted"));
setShowRating(false);
setRatingComment("");
+ loadOrder();
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response
@@ -133,6 +177,7 @@ export default function OrderDetailPage() {
toast.success(t("shops.reportSubmitted"));
setShowReport(false);
setReportText("");
+ loadOrder();
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response
@@ -150,6 +195,7 @@ export default function OrderDetailPage() {
toast.success(t("shops.returnRequestSubmitted"));
setShowReturn(false);
setReturnReason("");
+ loadOrder();
} catch (err: unknown) {
const message =
(err as { response?: { data?: { message?: string } } })?.response
@@ -209,235 +255,329 @@ export default function OrderDetailPage() {
.filter(Boolean)
.join(" ");
+ const boldTracking =
+ order.shipping_method && BOLD_TRACKING_METHODS.includes(order.shipping_method);
+ const trackingHref =
+ order.tracking_code && order.shipping_method && boldTracking
+ ? trackingUrl(order.shipping_method, order.tracking_code)
+ : null;
+
return (
-
-
-
-
{t("shops.orderDetailTitle")}
+ <>
+
+
+
+
+
{t("shops.orderDetailTitle")}
+
+ {t("shops.orderNumberLabel")}: {order.order_number}
+
-
-
- {primaryImage && (
-
-
-
- )}
-
-
{order.listing.title}
-
- {order.total_amount.toLocaleString()} {t("settings.toman")}
-
-
-
-
-
-
{t("shops.buyerInfoTitle")}
-
{buyerName}
-
- {order.buyerAddressSnapshot?.province?.name}{" "}
- {order.buyerAddressSnapshot?.city?.name}
-
-
{order.buyerAddressSnapshot?.address}
-
{order.createdAt}
- {order.shipping_method && (
-
- {t(`shops.shippingMethods.${order.shipping_method}`)}
-
- )}
- {order.tracking_code &&
- (order.shipping_method === "post" ? (
-
- {t("shops.trackingCodeLabel")}: {order.tracking_code}
-
- ) : (
-
- {t("shops.trackingCodeLabel")}: {order.tracking_code}
-
- ))}
-
-
-
-
setPrintMode("label-a5")}>
- {t("shops.printLabelA5")}
-
-
setPrintMode("label-a6")}>
- {t("shops.printLabelA6")}
-
-
-
setPrintMode("invoice")}>
- {t("shops.printInvoice")}
-
-
- {isSeller && (
- <>
-
-
{t("shops.trackingCodeLabel")}
-
-
setTrackingCode(e.target.value)}
+
+
+ {primaryImage && (
+
+
-
- {t("common.save")}
-
-
-
-
-
{t("shops.changeStatusLabel")}
-
- {SELLER_STATUSES.map((status) => (
-
- ))}
-
-
- >
- )}
-
- {isBuyer && (
- <>
- {canConfirmReceipt && (
-
- {t("shops.confirmReceipt")}
-
)}
+
+
+ {order.listing.title}{" "}
+ × {order.quantity}
+
+
+ {order.total_amount.toLocaleString()} {t("settings.toman")}
+
+
+
- setShowRating((v) => !v)}>
- {t("shops.rateShop")}
+ {isSeller && (
+
+ )}
+
+ {isBuyer && (
+
+ )}
+
+
+
+ {t("shops.shippingTimeLabel")}
+
+ {[order.createdAt, order.shop.estimated_delivery_text]
+ .filter(Boolean)
+ .join(" ")}
+
+
+ {order.shipping_method && (
+
+ {t("shops.shippingTypeLabel")}
+ {t(`shops.shippingMethods.${order.shipping_method}`)}
+
+ )}
+ {order.tracking_code && (
+
+ )}
+
+ {t("shops.shippingCostLabel")}
+
+ {order.shipping_cost.toLocaleString()} {t("settings.toman")}
+
+
+
+ {t("shops.grandTotalLabel")}
+
+ {order.total_amount.toLocaleString()} {t("settings.toman")}
+
+
+
+
+ {isSeller && (
+ setPrintMode("invoice")}>
+ {t("shops.printInvoice")}
- {showRating && (
-
-
- {[1, 2, 3, 4, 5].map((n) => (
+ )}
+
+ {isSeller && (
+ <>
+
+
{t("shops.trackingCodeLabel")}
+
+
setTrackingCode(e.target.value)}
+ />
+
+ {t("common.save")}
+
+
+
+
+
+
{t("shops.changeStatusLabel")}
+
+ {SELLER_STATUSES.map((status) => (
))}
-
- )}
-
setShowReport((v) => !v)}>
- {t("shops.reportProblem")}
-
- {showReport && (
-
- )}
+ {report && (
+
+
{t("shops.reportStatusTitle")}
+
{report.description}
+
+ {t(`shops.reportStatus.${report.status}`)}
+
+
+ )}
- {canRequestReturn && (
- <>
-
setShowReturn((v) => !v)}>
- {t("shops.requestReturn")}
+ {returnRequest && (
+
+
{t("shops.returnStatusTitle")}
+
{returnRequest.reason}
+
+ {t(`shops.returnStatus.${returnRequest.status}`)}
+
+
+ )}
+ >
+ )}
+
+ {isBuyer && (
+ <>
+ {canConfirmReceipt && (
+
+ {t("shops.confirmReceipt")}
- {showReturn && (
-
-
+
+ {shopRating.comment && (
+
+ {shopRating.comment}
+
+ )}
+
+ ) : (
+ <>
+ setShowRating((v) => !v)}>
+ {t("shops.rateShop")}
+
+ {showRating && (
+
+
+ {[1, 2, 3, 4, 5].map((n) => (
+
+ ))}
+
+
+ )}
+ >
+ )}
- {printMode && (
-
- {(printMode === "label-a5" || printMode === "label-a6") && (
-
-
{order.shop.name}
-
{t("shops.buyerInfoTitle")}:
-
{buyerName}
-
- {order.buyerAddressSnapshot?.province?.name}{" "}
- {order.buyerAddressSnapshot?.city?.name}
-
-
{order.buyerAddressSnapshot?.address}
- {order.tracking_code && (
-
- {t("shops.trackingCodeLabel")}: {order.tracking_code}
-
- )}
-
- )}
- {printMode === "invoice" && (
+ {report ? (
+
+
{t("shops.reportStatusTitle")}
+
{report.description}
+
+ {t(`shops.reportStatus.${report.status}`)}
+
+
+ ) : (
+ <>
+
setShowReport((v) => !v)}>
+ {t("shops.reportProblem")}
+
+ {showReport && (
+
+ )}
+ >
+ )}
+
+ {returnRequest ? (
+
+
{t("shops.returnStatusTitle")}
+
{returnRequest.reason}
+
+ {t(`shops.returnStatus.${returnRequest.status}`)}
+
+
+ ) : (
+ canRequestReturn && (
+ <>
+
setShowReturn((v) => !v)}>
+ {t("shops.requestReturn")}
+
+ {showReturn && (
+
+ )}
+ >
+ )
+ )}
+ >
+ )}
+
+
+
+ {printMode === "invoice" && (
+
{t("shops.invoiceTitle")}
{order.listing.title}
@@ -450,19 +590,30 @@ export default function OrderDetailPage() {
{buyerName}
{order.createdAt}
- )}
-
- )}
+
+ )}
-
-
-
+
+
+
+
+
setShowBuyerInfo(false)}
+ buyer={order.buyerAddressSnapshot}
+ />
+ setShowShopInfo(false)}
+ shop={order.shop}
+ />
+ >
);
}
diff --git a/src/app/shops/profile/[shopId]/page.tsx b/src/app/shops/profile/[shopId]/page.tsx
new file mode 100644
index 0000000..37de4e2
--- /dev/null
+++ b/src/app/shops/profile/[shopId]/page.tsx
@@ -0,0 +1,384 @@
+"use client";
+
+import Container from "@/components/elements/Container";
+import LocalePageShell from "@/components/i18n/LocalePageShell";
+import Header from "@/components/main/Header";
+import TabNavigation from "@/components/TabNavigation";
+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 ShopContactInfoModal from "@/components/shops/ShopContactInfoModal";
+import ShopMyOrdersModal from "@/components/shops/ShopMyOrdersModal";
+import useAxios from "@/hooks/useAxios";
+import useInfiniteScroll from "@/hooks/useInfiniteScroll";
+import { useUser } from "@/hooks/useUser";
+import { useRequireAuth } from "@/lib/auth/useRequireAuth";
+import { profileActionBtnClass } from "@/lib/ui/buttonStyles";
+import { cn } from "@/lib/utils";
+import { IShopProductListing } from "@/types/types";
+import dynamic from "next/dynamic";
+import Image from "next/image";
+import { useParams, useRouter } from "next/navigation";
+import { useEffect, useState } from "react";
+import toast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+
+const LocationModal = dynamic(
+ () => import("@/components/models/ModelPage/LocationModal"),
+ { ssr: false }
+);
+
+type PublicShop = {
+ _id: string;
+ name: string;
+ logo?: string | null;
+ description?: string | null;
+ has_physical_location?: boolean;
+ province?: { name: string } | null;
+ city?: { name: string } | null;
+ address?: string | null;
+ lat?: string | null;
+ lng?: string | null;
+ contact?: {
+ mobile?: string | null;
+ landline?: string | null;
+ telegram?: string | null;
+ whatsapp?: string | null;
+ instagram?: string | null;
+ } | null;
+ response_schedule?: { day: string; start_time?: string | null; end_time?: string | null }[];
+ owner?: {
+ _id: string;
+ user_name: string;
+ } | null;
+ productCount: number;
+ rating: { average: number; count: number };
+ isFollowing: boolean;
+ followedAt?: string | null;
+ isOwner: boolean;
+ createdAt?: string;
+};
+
+type PerformanceTier = "weak" | "average" | "good" | "veryGood" | "excellent";
+
+function getPerformanceTier(percent: number): PerformanceTier {
+ if (percent < 60) return "weak";
+ if (percent < 70) return "average";
+ if (percent < 80) return "good";
+ if (percent < 90) return "veryGood";
+ return "excellent";
+}
+
+function monthsSince(dateStr: string): number {
+ const diffMs = Date.now() - new Date(dateStr).getTime();
+ return Math.floor(diffMs / (1000 * 60 * 60 * 24 * 30));
+}
+
+export default function ShopPublicProfilePage() {
+ const { t, i18n } = useTranslation("common");
+ const isFa = (i18n.language || "fa").toLowerCase().startsWith("fa");
+ const router = useRouter();
+ const params = useParams<{ shopId: string }>();
+ const shopId = params.shopId;
+ const { request } = useAxios();
+ const requireAuth = useRequireAuth();
+ const currentUser = useUser();
+
+ const [shop, setShop] = useState(null);
+ const [isFollowing, setIsFollowing] = useState(false);
+ const [followedAt, setFollowedAt] = useState(null);
+ const [followLoading, setFollowLoading] = useState(false);
+ const [showContactModal, setShowContactModal] = useState(false);
+ const [showOrdersModal, setShowOrdersModal] = useState(false);
+ const [showLocationModal, setShowLocationModal] = useState(false);
+ const [showShareModal, setShowShareModal] = useState(false);
+
+ useEffect(() => {
+ request<{ shop: PublicShop }>("GET", `/shops/public/${shopId}`)
+ .then((res) => {
+ const data = res?.shop || null;
+ setShop(data);
+ setIsFollowing(Boolean(data?.isFollowing));
+ setFollowedAt(data?.followedAt || null);
+ })
+ .catch(() => setShop(null));
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [shopId]);
+
+ const { data, isLoading } = useInfiniteScroll({
+ endpoint: "/shop-products",
+ queryKey: ["shop-products", shopId],
+ params: { shopId },
+ });
+ const listings: IShopProductListing[] =
+ data?.pages.flatMap((page) => page.docs || []) || [];
+
+ const toggleFollow = async () => {
+ if (!requireAuth() || !shop || followLoading) return;
+ const nextFollowing = !isFollowing;
+ setIsFollowing(nextFollowing);
+ setFollowLoading(true);
+ try {
+ const res = await request<{ is_following: boolean; followed_at: string | null }>(
+ "POST",
+ `/shops/${shop._id}/follow`,
+ {}
+ );
+ setIsFollowing(res?.is_following ?? nextFollowing);
+ setFollowedAt(res?.followed_at || null);
+ toast.success(
+ (res?.is_following ?? nextFollowing)
+ ? t("shops.shopFollowSuccess")
+ : t("shops.shopUnfollowSuccess")
+ );
+ } catch {
+ setIsFollowing(!nextFollowing);
+ } finally {
+ setFollowLoading(false);
+ }
+ };
+
+ const handleMessage = () => {
+ if (!requireAuth() || !shop || !currentUser?._id) return;
+ router.push(`/settings/chats/shop/${shop._id}/${currentUser._id}`);
+ };
+
+ if (!shop) return null;
+
+ const percent =
+ shop.rating.count > 0 ? Math.round((shop.rating.average / 5) * 100) : null;
+ const performanceTier = percent != null ? getPerformanceTier(percent) : null;
+ const hasDescription = Boolean(shop.description?.trim());
+
+ const shopTenureLabel = shop.createdAt
+ ? monthsSince(shop.createdAt) < 1
+ ? t("shops.lessThanOneMonth")
+ : t("shops.monthsCount", { count: monthsSince(shop.createdAt) })
+ : t("shops.followButton");
+
+ const followLabel = isFollowing
+ ? followedAt
+ ? monthsSince(followedAt) < 1
+ ? t("shops.lessThanOneMonth")
+ : t("shops.monthsCount", { count: monthsSince(followedAt) })
+ : t("shops.unfollowButton")
+ : shopTenureLabel;
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+ {shop.productCount}
+ {t("shops.productsLabel")}
+
+
+
+ {performanceTier
+ ? t(`shops.performance.${performanceTier}`)
+ : t("shops.underReview")}
+
+ {t("shops.performanceLabel")}
+
+
+ {percent != null ? `${percent}%` : t("shops.newShop")}
+ {t("shops.satisfactionLabel")}
+
+
+
+
+
+
+
+
+ {shop.rating.count > 0 ? shop.rating.average.toFixed(1) : "0"}
+
+
+
+ {shop.rating.count}
+
+
+
+
+ {shop.name}
+
+
+
+
+
+
+ {hasDescription ? (
+
+
+
+ ) : null}
+
+ {shop.has_physical_location ? t("shops.inPerson") : t("shops.onlineOnly")}
+
+
+
+
+
+
+
+
+
+ {shop.has_physical_location && (
+
+ )}
+
+
+
+
+ {isLoading ? (
+
+ {t("common.loading")}
+
+ ) : listings.length === 0 ? (
+
+ {t("shops.noProductsYet")}
+
+ ) : (
+
+ {listings.map((listing) => {
+ const primaryImage =
+ listing.images?.[listing.primaryImageIndex] || listing.images?.[0];
+ return (
+
+ );
+ })}
+
+ )}
+
+
+
+
+
+
+ setShowShareModal(false)}
+ shopId={shop._id}
+ shopName={shop.name}
+ shopLogo={shop.logo}
+ responseSchedule={shop.response_schedule}
+ />
+ setShowContactModal(false)}
+ contactInfo={shop.contact || {}}
+ />
+ setShowOrdersModal(false)}
+ shopId={shop._id}
+ />
+ {showLocationModal && shop.has_physical_location && (
+ setShowLocationModal(false)}
+ location={{
+ address: shop.address || undefined,
+ lat: shop.lat || undefined,
+ lng: shop.lng || undefined,
+ city: shop.city || undefined,
+ province: shop.province || undefined,
+ }}
+ />
+ )}
+ >
+ );
+}
diff --git a/src/app/shops/profile/[shopId]/reel/[listingId]/page.tsx b/src/app/shops/profile/[shopId]/reel/[listingId]/page.tsx
new file mode 100644
index 0000000..84175f6
--- /dev/null
+++ b/src/app/shops/profile/[shopId]/reel/[listingId]/page.tsx
@@ -0,0 +1,10 @@
+"use client";
+
+import ShopReelsView from "@/components/shops/ShopReelsView";
+import { useParams } from "next/navigation";
+
+export default function ShopReelPage() {
+ const params = useParams<{ shopId: string; listingId: string }>();
+
+ return ;
+}
diff --git a/src/components/auth/AuthPageLayout.tsx b/src/components/auth/AuthPageLayout.tsx
index d10a84c..a4c77f0 100644
--- a/src/components/auth/AuthPageLayout.tsx
+++ b/src/components/auth/AuthPageLayout.tsx
@@ -71,7 +71,7 @@ function AuthPageLayout({ children, className }: AuthPageLayoutProps) {
diff --git a/src/components/chat/ChatMessageCard.tsx b/src/components/chat/ChatMessageCard.tsx
index 253a85b..8fb1ac2 100644
--- a/src/components/chat/ChatMessageCard.tsx
+++ b/src/components/chat/ChatMessageCard.tsx
@@ -38,6 +38,7 @@ import {
getMessageRowSpacing,
} from "@/lib/chat/messageGrouping";
import SharedPostBubble, { parseSharedPost } from "./SharedPostBubble";
+import SharedProductBubble, { parseSharedProduct } from "./SharedProductBubble";
import SharedStoryBubble, {
parseSharedStory,
extractStoryReactionText,
@@ -318,6 +319,7 @@ const ChatMessageCard = ({
const forwarded =
message.forwardedFrom || parseForwarded(message.content || "");
const sharedPost = parseSharedPost(message.content || "");
+ const sharedProduct = parseSharedProduct(message.content || "");
const sharedStory = parseSharedStory(message.content || "");
const storyReactionText = sharedStory
? extractStoryReactionText(message.content || "")
@@ -331,6 +333,7 @@ const ChatMessageCard = ({
!message.file &&
!hasLocation &&
!sharedPost &&
+ !sharedProduct &&
!sharedStory &&
!(message.viewOnce && isViewOnceMediaType(message.fileType))
) {
@@ -735,6 +738,16 @@ const ChatMessageCard = ({
)}
+ {sharedProduct && (
+
+
+
+
+ )}
+
{showTextBubble && !message.file && emojiOnly && (
{textContent}
)}
diff --git a/src/components/chat/ForwardMessageModal.tsx b/src/components/chat/ForwardMessageModal.tsx
index c1d8099..49a9ea6 100644
--- a/src/components/chat/ForwardMessageModal.tsx
+++ b/src/components/chat/ForwardMessageModal.tsx
@@ -58,7 +58,9 @@ export default function ForwardMessageModal({
{ noToast: true }
);
setUsers(
- (res?.filteredUsersData || []).filter((u) => u._id !== currentUserId)
+ (res?.filteredUsersData || []).filter(
+ (u) => u._id !== currentUserId && Boolean(u.user_name)
+ )
);
} finally {
setLoading(false);
diff --git a/src/components/chat/SharedProductBubble.tsx b/src/components/chat/SharedProductBubble.tsx
new file mode 100644
index 0000000..03020ab
--- /dev/null
+++ b/src/components/chat/SharedProductBubble.tsx
@@ -0,0 +1,59 @@
+"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 SharedProductPayload {
+ listingId: string;
+ title?: string;
+ preview?: string;
+ price?: number | null;
+ shopName?: string;
+ url?: string;
+}
+
+export function parseSharedProduct(content: string): SharedProductPayload | null {
+ if (!content) return null;
+ try {
+ const line = content.split("\n")[0];
+ const j = JSON.parse(line);
+ if (j?.e2ee) return null;
+ return j.sharedProduct ?? null;
+ } catch {
+ return null;
+ }
+}
+
+export default function SharedProductBubble({ data }: { data: SharedProductPayload }) {
+ const { t } = useTranslation("common");
+ const href = data.url || `/shops/listing/${data.listingId}`;
+ const preview = data.preview ? IMAGE_BASE_URL + data.preview : null;
+
+ return (
+
+ {preview && (
+
+
+
+ )}
+
+
+ {data.title || t("shops.viewProduct")}
+
+ {data.price != null && (
+
+ {t("shops.priceLabel", { amount: data.price.toLocaleString() })}
+
+ )}
+ {data.shopName ? (
+
{data.shopName}
+ ) : null}
+
+
+ );
+}
diff --git a/src/components/posts/SendPostModal.tsx b/src/components/posts/SendPostModal.tsx
index a6a7eb9..5612b2e 100644
--- a/src/components/posts/SendPostModal.tsx
+++ b/src/components/posts/SendPostModal.tsx
@@ -54,7 +54,9 @@ export default function SendPostModal({
{ noToast: true }
);
setUsers(
- (res?.filteredUsersData || []).filter((u) => u._id !== currentUserId)
+ (res?.filteredUsersData || []).filter(
+ (u) => u._id !== currentUserId && Boolean(u.user_name)
+ )
);
} finally {
setLoading(false);
diff --git a/src/components/shops/ProductCategoryPicker.tsx b/src/components/shops/ProductCategoryPicker.tsx
new file mode 100644
index 0000000..b4cabd0
--- /dev/null
+++ b/src/components/shops/ProductCategoryPicker.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { IShopCategory } from "@/types/types";
+import { toggleBtnClass } from "@/lib/ui/buttonStyles";
+import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
+
+type ProductCategoryPickerProps = {
+ categoryList: IShopCategory[] | null;
+ category: string;
+ subCategory: string;
+ subSubCategory: string;
+ onCategorySelect: (category: string) => void;
+ onSubCategorySelect: (subCategory: string) => void;
+ onSubSubCategorySelect: (subSubCategory: string) => void;
+};
+
+export default function ProductCategoryPicker({
+ categoryList,
+ category,
+ subCategory,
+ subSubCategory,
+ onCategorySelect,
+ onSubCategorySelect,
+ onSubSubCategorySelect,
+}: ProductCategoryPickerProps) {
+ const { t } = useTranslation("common");
+ const mainCount = categoryList?.length ?? 0;
+ const selectedCategory = categoryList?.find(
+ (item) => item.category === category
+ );
+ const selectedSubCategory = selectedCategory?.sub_categories.find(
+ (item) => item.name === subCategory
+ );
+
+ if (!mainCount) {
+ return (
+
+ {t("shops.loadingCategories")}
+
+ );
+ }
+
+ return (
+
+
+
+ {t("shops.selectMainCategory")}
+
+
+ {categoryList?.map((item) => (
+
+ ))}
+
+
+
+ {selectedCategory && selectedCategory.sub_categories.length > 0 && (
+
+
+ {t("shops.selectSubCategory")}
+
+
+ {selectedCategory.sub_categories.map((item) => (
+
+ ))}
+
+
+ )}
+
+ {selectedSubCategory &&
+ (selectedSubCategory.sub_sub_categories?.length ?? 0) > 0 && (
+
+
+ {t("shops.selectSubSubCategory")}
+
+
+ {selectedSubCategory.sub_sub_categories?.map((item) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/src/components/shops/ProductImageLightbox.tsx b/src/components/shops/ProductImageLightbox.tsx
new file mode 100644
index 0000000..e2bc624
--- /dev/null
+++ b/src/components/shops/ProductImageLightbox.tsx
@@ -0,0 +1,81 @@
+"use client";
+
+import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
+import BoldIcon from "@/components/ui/BoldIcon";
+import Image from "next/image";
+import { useEffect, useRef, useState } from "react";
+
+type ProductImageLightboxProps = {
+ images: string[];
+ initialIndex?: number;
+ alt: string;
+ open: boolean;
+ onClose: () => void;
+};
+
+export default function ProductImageLightbox({
+ images,
+ initialIndex = 0,
+ alt,
+ open,
+ onClose,
+}: ProductImageLightboxProps) {
+ const scrollRef = useRef(null);
+ const [activeIndex, setActiveIndex] = useState(initialIndex);
+
+ useEffect(() => {
+ if (!open || !scrollRef.current) return;
+ scrollRef.current.scrollTo({
+ left: initialIndex * scrollRef.current.clientWidth,
+ behavior: "auto",
+ });
+ setActiveIndex(initialIndex);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [open, initialIndex]);
+
+ if (!open) return null;
+
+ const handleScroll = (event: React.UIEvent) => {
+ const el = event.currentTarget;
+ const index = Math.round(el.scrollLeft / el.clientWidth);
+ setActiveIndex(index);
+ };
+
+ return (
+
+
+
+ {images.length > 1 && (
+
+ {activeIndex + 1}/{images.length}
+
+ )}
+
+
+ {images.map((img) => (
+
+
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/shops/SendProductModal.tsx b/src/components/shops/SendProductModal.tsx
new file mode 100644
index 0000000..1ee3b14
--- /dev/null
+++ b/src/components/shops/SendProductModal.tsx
@@ -0,0 +1,210 @@
+"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 { IShopProductListing } from "@/types/types";
+import { useTranslation } from "react-i18next";
+
+export interface ChatUserItem {
+ _id: string;
+ user_name: string;
+ first_name: string;
+ last_name: string;
+ profile_image?: string;
+}
+
+interface SendProductModalProps {
+ open: boolean;
+ onClose: () => void;
+ listing: IShopProductListing;
+}
+
+export default function SendProductModal({
+ open,
+ onClose,
+ listing,
+}: SendProductModalProps) {
+ const { t } = useTranslation("common");
+ const { request } = useAxios();
+ const [users, setUsers] = useState([]);
+ const [selected, setSelected] = useState>(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",
+ `/shop-products/${listing._id}/send`,
+ { receiverIds: Array.from(selected) },
+ { noToast: true }
+ );
+ toast.success(
+ t("shops.sentProductToCount", { count: res?.sentCount ?? selected.size })
+ );
+ onClose();
+ setSelected(new Set());
+ } catch {
+ toast.error(t("shops.sendProductError"));
+ } finally {
+ setSending(false);
+ }
+ };
+
+ const preview =
+ listing.images?.[listing.primaryImageIndex] || listing.images?.[0] || null;
+
+ return (
+
+ {open && (
+
+ e.stopPropagation()}
+ >
+
+ {t("shops.sendProductTitle")}
+
+
+
+ {preview && (
+
+
+
+ )}
+
+ {listing.title}
+
+
+
+ {loading ? (
+
+
+
+ ) : users.length === 0 ? (
+
+ {t("posts.noChatUsers")}
+
+ ) : (
+
+ {users.map((u) => {
+ const name =
+ [u.first_name, u.last_name].filter(Boolean).join(" ") ||
+ u.user_name;
+ const checked = selected.has(u._id);
+ return (
+ -
+
+
+ );
+ })}
+
+ )}
+
+
+
+
+ )}
+
+ );
+}
diff --git a/src/components/shops/ShareProductModal.tsx b/src/components/shops/ShareProductModal.tsx
new file mode 100644
index 0000000..dc157b7
--- /dev/null
+++ b/src/components/shops/ShareProductModal.tsx
@@ -0,0 +1,115 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Image from "next/image";
+import Modal from "@/components/elements/Modal";
+import ProfileAvatar from "@/components/main/ProfileAvatar";
+import RoundedButton from "@/components/elements/RoundedButton";
+import toast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+import { copyTextToClipboard } from "@/lib/chat/getCopyableMessageText";
+
+type ShareProductModalProps = {
+ open: boolean;
+ onClose: () => void;
+ listingId: string;
+ title?: string | null;
+ image?: string | null;
+};
+
+export default function ShareProductModal({
+ open,
+ onClose,
+ listingId,
+ title,
+ image,
+}: ShareProductModalProps) {
+ const { t } = useTranslation("common");
+ const [copied, setCopied] = useState(false);
+
+ const productUrl = useMemo(() => {
+ if (!listingId) return "";
+ if (typeof window !== "undefined") {
+ return `${window.location.origin}/shops/listing/${listingId}`;
+ }
+ return `https://modstagram.com/shops/listing/${listingId}`;
+ }, [listingId]);
+
+ const qrSrc = useMemo(() => {
+ if (!productUrl) return "";
+ return `https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=12&data=${encodeURIComponent(productUrl)}`;
+ }, [productUrl]);
+
+ const handleCopy = async () => {
+ if (!productUrl) return;
+ const ok = await copyTextToClipboard(productUrl);
+ if (ok) {
+ setCopied(true);
+ toast.success(t("shops.share.copied"));
+ window.setTimeout(() => setCopied(false), 2000);
+ } else {
+ toast.error(t("shops.share.copyFailed"));
+ }
+ };
+
+ return (
+
+
+
{t("shops.share.title")}
+
+
+
+ {title ?
{title}
: null}
+
+
+ {qrSrc ? (
+
+
+
+ ) : null}
+
+
+ {t("shops.share.linkLabel")}
+
+
+ {productUrl || "—"}
+
+
+
+
+ {t("shops.share.close")}
+
+ void handleCopy()}
+ className="!border-transparent h-9 w-full !bg-sky-100 text-sky-600"
+ >
+ {copied ? t("shops.share.copied") : t("shops.share.copyLink")}
+
+
+
+
+ );
+}
diff --git a/src/components/shops/ShareShopModal.tsx b/src/components/shops/ShareShopModal.tsx
new file mode 100644
index 0000000..5a1c6c3
--- /dev/null
+++ b/src/components/shops/ShareShopModal.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { useMemo, useState } from "react";
+import Image from "next/image";
+import Modal from "@/components/elements/Modal";
+import ProfileAvatar from "@/components/main/ProfileAvatar";
+import RoundedButton from "@/components/elements/RoundedButton";
+import toast from "react-hot-toast";
+import { useTranslation } from "react-i18next";
+import { copyTextToClipboard } from "@/lib/chat/getCopyableMessageText";
+
+type ResponseScheduleItem = {
+ day: string;
+ start_time?: string | null;
+ end_time?: string | null;
+};
+
+type ShareShopModalProps = {
+ open: boolean;
+ onClose: () => void;
+ shopId: string;
+ shopName?: string | null;
+ shopLogo?: string | null;
+ responseSchedule?: ResponseScheduleItem[];
+};
+
+export default function ShareShopModal({
+ open,
+ onClose,
+ shopId,
+ shopName,
+ shopLogo,
+ responseSchedule,
+}: ShareShopModalProps) {
+ const { t } = useTranslation("common");
+ const [copied, setCopied] = useState(false);
+
+ const shopUrl = useMemo(() => {
+ if (!shopId) return "";
+ if (typeof window !== "undefined") {
+ return `${window.location.origin}/shops/profile/${shopId}`;
+ }
+ return `https://modstagram.com/shops/profile/${shopId}`;
+ }, [shopId]);
+
+ const qrSrc = useMemo(() => {
+ if (!shopUrl) return "";
+ return `https://api.qrserver.com/v1/create-qr-code/?size=220x220&margin=12&data=${encodeURIComponent(shopUrl)}`;
+ }, [shopUrl]);
+
+ const handleCopy = async () => {
+ if (!shopUrl) return;
+ const ok = await copyTextToClipboard(shopUrl);
+ if (ok) {
+ setCopied(true);
+ toast.success(t("shops.share.copied"));
+ window.setTimeout(() => setCopied(false), 2000);
+ } else {
+ toast.error(t("shops.share.copyFailed"));
+ }
+ };
+
+ return (
+
+
+
{t("shops.share.title")}
+
+
+
+ {shopName ?
{shopName}
: null}
+
+
+ {qrSrc ? (
+
+
+
+ ) : null}
+
+
+ {t("shops.share.linkLabel")}
+
+
+ {shopUrl || "—"}
+
+
+ {responseSchedule && responseSchedule.length > 0 && (
+
+
{t("shops.responseScheduleTitle")}
+
+ {responseSchedule.map((item) => (
+
+
+ {item.start_time || "--:--"} - {item.end_time || "--:--"}
+
+ {t(`shops.days.${item.day}`)}
+
+ ))}
+
+
+ )}
+
+
+
+ {t("shops.share.close")}
+
+ void handleCopy()}
+ className="!border-transparent h-9 w-full !bg-sky-100 text-sky-600"
+ >
+ {copied ? t("shops.share.copied") : t("shops.share.copyLink")}
+
+
+
+
+ );
+}
diff --git a/src/components/shops/ShopCategoryPicker.tsx b/src/components/shops/ShopCategoryPicker.tsx
index 2e4e1af..24ff457 100644
--- a/src/components/shops/ShopCategoryPicker.tsx
+++ b/src/components/shops/ShopCategoryPicker.tsx
@@ -6,19 +6,7 @@ import { toggleBtnClass } from "@/lib/ui/buttonStyles";
import BoldIcon from "@/components/ui/BoldIcon";
import { useTranslation } from "react-i18next";
-function gridColsClass(count: number): string {
- if (count <= 1) return "grid-cols-1";
- if (count === 2) return "grid-cols-2";
- if (count === 3) return "grid-cols-3";
- if (count === 4) return "grid-cols-4";
- if (count === 5) return "grid-cols-5";
- return "grid-cols-3 sm:grid-cols-4 md:grid-cols-5";
-}
-
-function gridMaxWidth(count: number): number {
- const cols = Math.min(Math.max(count, 1), 5);
- return cols * 108;
-}
+const FIXED_GRID_MAX_WIDTH = 440;
type ShopCategoryPickerProps = {
categoryList: IShopCategory[] | null;
@@ -57,8 +45,8 @@ export default function ShopCategoryPicker({
return (
{categoryList?.map((item) => (
diff --git a/src/components/shops/orders/SellerOrderItem.tsx b/src/components/shops/orders/SellerOrderItem.tsx
index a9b12e8..834c32a 100644
--- a/src/components/shops/orders/SellerOrderItem.tsx
+++ b/src/components/shops/orders/SellerOrderItem.tsx
@@ -6,6 +6,7 @@ import { useTranslation } from "react-i18next";
type SellerOrder = {
_id: string;
+ order_number?: string;
quantity: number;
total_amount: number;
status: string;
@@ -64,6 +65,11 @@ export default function SellerOrderItem({ order }: { order: SellerOrder }) {
{order.total_amount.toLocaleString()} {t("settings.toman")}
+ {order.order_number && (
+
+ {t("shops.orderNumberLabel")}: {order.order_number}
+
+ )}
diff --git a/src/components/shops/orders/ShopInfoModal.tsx b/src/components/shops/orders/ShopInfoModal.tsx
new file mode 100644
index 0000000..0a29201
--- /dev/null
+++ b/src/components/shops/orders/ShopInfoModal.tsx
@@ -0,0 +1,149 @@
+"use client";
+
+import { useEffect, useState } from "react";
+import Link from "next/link";
+import BoldIcon from "@/components/ui/BoldIcon";
+import { btnPrimary, btnDefault } from "@/lib/ui/buttonStyles";
+import { cn } from "@/lib/utils";
+import { useTranslation } from "react-i18next";
+
+type ShopInfo = {
+ _id: string;
+ name: string;
+ has_physical_location?: boolean;
+ province?: { name?: string } | null;
+ city?: { name?: string } | null;
+ address?: string | null;
+ lat?: string | null;
+ lng?: string | null;
+};
+
+type ShopInfoModalProps = {
+ isOpen: boolean;
+ onClose: () => void;
+ shop?: ShopInfo | null;
+};
+
+export default function ShopInfoModal({ isOpen, onClose, shop }: ShopInfoModalProps) {
+ const { t } = useTranslation("common");
+ const [coords, setCoords] = useState<[number, number] | null>(null);
+
+ useEffect(() => {
+ if (!isOpen) {
+ setCoords(null);
+ return;
+ }
+ const lat = parseFloat(shop?.lat || "");
+ const lng = parseFloat(shop?.lng || "");
+ if (!isNaN(lat) && !isNaN(lng)) {
+ setCoords([lat, lng]);
+ } else {
+ setCoords(null);
+ }
+ }, [shop, isOpen]);
+
+ if (!isOpen || !shop) return null;
+
+ const googleMapsUrl = coords
+ ? `https://www.google.com/maps/dir/?api=1&destination=${coords[0]},${coords[1]}`
+ : "https://www.google.com/maps";
+
+ return (
+
+
+
+
+
+ {t("shops.shopInfoTitle")}
+
+
+
+
+
+ {shop.name}
+
+ {shop.has_physical_location && (
+ <>
+
+
+ {t("models.location.city")}{" "}
+
+ {shop.city?.name || t("models.location.unknown")}،{" "}
+ {shop.province?.name || t("models.location.unknown")}
+
+ {shop.address && (
+
+
+ {t("models.location.address")}{" "}
+
+ {shop.address}
+
+ )}
+ >
+ )}
+
+
+
+ {t("shops.viewShopProfileButton")}
+
+
+ {shop.has_physical_location &&
+ (coords ? (
+
+
+
+ ) : (
+
{t("models.location.invalidCoords")}
+ ))}
+
+
+
+
+
+
+ );
+}
diff --git a/src/components/stories/SendStoryModal.tsx b/src/components/stories/SendStoryModal.tsx
index 73a92b0..93f8e36 100644
--- a/src/components/stories/SendStoryModal.tsx
+++ b/src/components/stories/SendStoryModal.tsx
@@ -54,7 +54,9 @@ export default function SendStoryModal({
{ noToast: true }
);
setUsers(
- (res?.filteredUsersData || []).filter((u) => u._id !== currentUserId)
+ (res?.filteredUsersData || []).filter(
+ (u) => u._id !== currentUserId && Boolean(u.user_name)
+ )
);
} finally {
setLoading(false);
diff --git a/src/hooks/useAxios.tsx b/src/hooks/useAxios.tsx
index 501c20d..d978bb4 100644
--- a/src/hooks/useAxios.tsx
+++ b/src/hooks/useAxios.tsx
@@ -179,19 +179,25 @@ const useAxios = () => {
try {
const isFormData = data instanceof FormData;
+ const headers: NonNullable
= {
+ ...config.headers,
+ ...(isFormData ? {} : { "Content-Type": "application/json" }),
+ ...(config.noToast ? { "X-No-Toast": "true" } : {}), // ارسال noToast به عنوان یک هدر
+ };
+ // مرورگر باید Content-Type مولتیپارت را همراه با boundary خودش بسازد؛
+ // مقداردهی دستی این هدر باعث حذف boundary و شکست پارس سمت سرور میشود.
+ if (isFormData) {
+ delete headers["Content-Type"];
+ delete headers["content-type"];
+ }
+
const response = await axiosInstance({
method,
url,
data,
baseURL: getApiBaseUrl(),
...config,
- headers: {
-
-
- ...config.headers,
- ...(isFormData ? {} : { "Content-Type": "application/json" }), // حذف Content-Type برای FormData
- ...(config.noToast ? { "X-No-Toast": "true" } : {}), // ارسال noToast به عنوان یک هدر
- },
+ headers,
});
return response.data;
diff --git a/src/hooks/useInfiniteScroll.tsx b/src/hooks/useInfiniteScroll.tsx
index 67dc14a..5c0770d 100644
--- a/src/hooks/useInfiniteScroll.tsx
+++ b/src/hooks/useInfiniteScroll.tsx
@@ -5,9 +5,11 @@ import useAxios from "@/hooks/useAxios";
interface FetchParams {
endpoint: string;
- queryKey: (string | number)[];
+ queryKey: (string | number)[];
params?: Record;
limit?: number;
+ /** Set to false to defer fetching until dependencies (e.g. an id) are ready. Defaults to true. */
+ enabled?: boolean;
}
const useInfiniteScroll = ({
@@ -15,6 +17,7 @@ const useInfiniteScroll = ({
queryKey,
params = {},
limit = 10,
+ enabled = true,
}: FetchParams) => {
const { request } = useAxios();
const containerRef = useRef(null);
@@ -41,6 +44,7 @@ const useInfiniteScroll = ({
lastPage.totalPages > allPages.length ? allPages.length + 1 : undefined,
staleTime: 0,
refetchOnMount: "always",
+ enabled,
});
useEffect(() => {
diff --git a/src/hooks/useProductWizardId.ts b/src/hooks/useProductWizardId.ts
index 22dc121..ae7389c 100644
--- a/src/hooks/useProductWizardId.ts
+++ b/src/hooks/useProductWizardId.ts
@@ -17,6 +17,12 @@ export function clearProductWizardId(shopId: string): void {
localStorage.removeItem(storageKey(shopId));
}
+/** Synchronous, no-redirect read — used by the name step to detect edit mode. */
+export function getProductWizardId(shopId: string): string | null {
+ if (typeof window === "undefined") return null;
+ return localStorage.getItem(storageKey(shopId));
+}
+
/** Reads the in-progress listing id saved by the product name step; redirects back to it if missing. */
export function useProductWizardId(shopId: string): string | null {
const router = useRouter();
diff --git a/src/hooks/useShopWizardId.ts b/src/hooks/useShopWizardId.ts
index afb16f9..7d8a561 100644
--- a/src/hooks/useShopWizardId.ts
+++ b/src/hooks/useShopWizardId.ts
@@ -3,16 +3,31 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
+const STORAGE_KEY = "shop_wizard_shop_id";
+
+export function saveShopWizardId(shopId: string): void {
+ if (typeof window === "undefined") return;
+ localStorage.setItem(STORAGE_KEY, shopId);
+}
+
+export function clearShopWizardId(): void {
+ if (typeof window === "undefined") return;
+ localStorage.removeItem(STORAGE_KEY);
+}
+
+/** Synchronous, no-redirect read — used by the name step to detect edit mode. */
+export function getShopWizardId(): string | null {
+ if (typeof window === "undefined") return null;
+ return localStorage.getItem(STORAGE_KEY);
+}
+
/** Reads the in-progress shop id saved by the name step; redirects back to it if missing. */
export function useShopWizardId(): string | null {
const router = useRouter();
const [shopId, setShopId] = useState(null);
useEffect(() => {
- const stored =
- typeof window !== "undefined"
- ? localStorage.getItem("shop_wizard_shop_id")
- : null;
+ const stored = getShopWizardId();
if (!stored) {
router.replace("/shops/new/name");
diff --git a/src/lib/chat/getCopyableMessageText.ts b/src/lib/chat/getCopyableMessageText.ts
index 14bc376..af60ad5 100644
--- a/src/lib/chat/getCopyableMessageText.ts
+++ b/src/lib/chat/getCopyableMessageText.ts
@@ -1,5 +1,6 @@
import { parseLocationContent } from "@/components/chat/LocationMessageBubble";
import { parseSharedPost } from "@/components/chat/SharedPostBubble";
+import { parseSharedProduct } from "@/components/chat/SharedProductBubble";
import {
extractStoryReactionText,
parseSharedStory,
@@ -54,6 +55,11 @@ export function getCopyableMessageText(
return null;
}
+ const sharedProduct = parseSharedProduct(content);
+ if (sharedProduct) {
+ return sharedProduct.title?.trim() || null;
+ }
+
const sharedStory = parseSharedStory(content);
if (sharedStory) {
const reactionText = extractStoryReactionText(content);
diff --git a/src/lib/shops/localCart.ts b/src/lib/shops/localCart.ts
new file mode 100644
index 0000000..ce70ec2
--- /dev/null
+++ b/src/lib/shops/localCart.ts
@@ -0,0 +1,63 @@
+"use client";
+
+export type CartItem = {
+ listingId: string;
+ variantId: string;
+ quantity: number;
+};
+
+const CART_KEY = "shop_local_cart";
+const FAVORITES_KEY = "shop_local_favorites";
+
+function readJson(key: string, fallback: T): T {
+ if (typeof window === "undefined") return fallback;
+ try {
+ const raw = localStorage.getItem(key);
+ return raw ? (JSON.parse(raw) as T) : fallback;
+ } catch {
+ return fallback;
+ }
+}
+
+function writeJson(key: string, value: T): void {
+ if (typeof window === "undefined") return;
+ localStorage.setItem(key, JSON.stringify(value));
+}
+
+export function getCart(): CartItem[] {
+ return readJson(CART_KEY, []);
+}
+
+export function addToCart(listingId: string, variantId: string, quantity = 1): void {
+ const cart = getCart();
+ const existing = cart.find(
+ (item) => item.listingId === listingId && item.variantId === variantId
+ );
+ if (existing) {
+ existing.quantity += quantity;
+ } else {
+ cart.push({ listingId, variantId, quantity });
+ }
+ writeJson(CART_KEY, cart);
+}
+
+export function getFavorites(): string[] {
+ return readJson(FAVORITES_KEY, []);
+}
+
+export function isFavorite(listingId: string): boolean {
+ return getFavorites().includes(listingId);
+}
+
+export function toggleFavorite(listingId: string): boolean {
+ const favorites = getFavorites();
+ const idx = favorites.indexOf(listingId);
+ if (idx >= 0) {
+ favorites.splice(idx, 1);
+ writeJson(FAVORITES_KEY, favorites);
+ return false;
+ }
+ favorites.push(listingId);
+ writeJson(FAVORITES_KEY, favorites);
+ return true;
+}
diff --git a/src/locales/en/common.json b/src/locales/en/common.json
index 922ca45..ae03513 100644
--- a/src/locales/en/common.json
+++ b/src/locales/en/common.json
@@ -2173,11 +2173,23 @@
"loadingCategories": "Loading categories...",
"selectSubCategory": "Select your sub-categories",
"subCategoryDisplayHint": "Check an item to show it on the shop page",
+ "subCategoryMaxHint": "You can select up to 3 sub-categories",
+ "subCategoryMaxReached": "You can select up to 3 sub-categories",
"displaySubCategoryAria": "Show {{name}} on the shop",
"unknownError": "An unknown error occurred",
"saveAndContinue": "Save and continue",
"nameTitle": "Shop name",
"namePlaceholder": "Enter shop name",
+ "descriptionPlaceholder": "Shop description (optional)",
+ "share": {
+ "title": "Share shop",
+ "linkLabel": "Shop link",
+ "copyLink": "Copy link",
+ "copied": "Link copied",
+ "copyFailed": "Copy failed",
+ "qrAlt": "Shop QR code",
+ "close": "Close"
+ },
"nameRequired": "Shop name is required",
"logoTitle": "Shop logo",
"selectLogo": "Select/edit logo",
@@ -2186,6 +2198,8 @@
"categoryRequired": "Category, sub-category, and a display choice are required",
"shippingTitle": "Shipping methods",
"shippingMethodRequired": "Select at least one shipping method",
+ "activeShippingMethods": "Active shipping methods",
+ "supplementarySettings": "Supplementary settings & services",
"shippingMethods": {
"post": "Post",
"tipax": "Tipax",
@@ -2204,9 +2218,11 @@
"neighbourhoodPlaceholder": "Neighbourhood (optional)",
"addressPlaceholder": "Address (optional)",
"contactTitle": "Shop support info",
- "phonePlaceholder": "Phone",
+ "landlinePlaceholder": "Landline",
"contactRequired": "Enter at least one contact method",
"responseScheduleTitle": "Response days and hours",
+ "responseStartTime": "Start time",
+ "responseEndTime": "End time",
"days": {
"sat": "Saturday",
"sun": "Sunday",
@@ -2217,11 +2233,87 @@
"fri": "Friday"
},
"previewTitle": "Shop preview",
+ "previewMinOrder": "Minimum order",
+ "previewDeliveryTime": "Delivery time",
+ "previewYes": "Yes",
"submitShop": "Submit shop",
"submitSuccess": "Shop submitted for review successfully",
"mySwitcher": "My shops",
"addShop": "Add shop/product",
"noShopsYet": "You haven't registered a shop yet",
+ "myShopsTitle": "My shops",
+ "myProductsTitle": "My products",
+ "addShopNew": "Add new shop",
+ "addProductNew": "Add product",
+ "searchShopPlaceholder": "Search shop",
+ "searchProductPlaceholder": "Search product",
+ "onlineShop": "Online shop",
+ "inPerson": "In person",
+ "onlineOnly": "Online",
+ "categoryLabel": "Category: {{category}}",
+ "stockCountLabel": "{{count}} pcs",
+ "specsTitle": "Product specifications",
+ "categorySpecLabel": "Category",
+ "sizeLabel": "Size",
+ "colorLabel": "Color",
+ "weightLabel": "Weight",
+ "descriptionLabel": "Product description",
+ "priceSpecLabel": "Price",
+ "showMore": "More",
+ "showLess": "Less",
+ "manageStockButton": "Manage stock",
+ "deactivateButton": "Deactivate",
+ "activateButton": "Activate",
+ "editProductButton": "Edit product info",
+ "editShopButton": "Edit shop",
+ "viewPublicProfile": "View shop page",
+ "viewProduct": "View product",
+ "myOrdersFromShop": "My orders from this shop",
+ "noOrdersFromShop": "You haven't ordered from this shop yet",
+ "satisfactionLabel": "Product satisfaction",
+ "performanceLabel": "Shop performance",
+ "noRatingYet": "No rating yet",
+ "productsLabel": "Products",
+ "underReview": "Under review",
+ "newShop": "New shop",
+ "placeOrderButton": "Place order",
+ "addToCartButton": "Add to cart",
+ "addToCartSuccess": "Added to cart",
+ "addToFavoritesButton": "Add to favorites",
+ "removeFromFavoritesButton": "Remove from favorites",
+ "addedToFavorites": "Added to favorites",
+ "removedFromFavorites": "Removed from favorites",
+ "sendProductTitle": "Send product",
+ "sentProductToCount": "Product sent to {{count}} people",
+ "sendProductError": "Failed to send product",
+ "noChatMessagesYet": "No messages yet",
+ "performance": {
+ "weak": "Weak",
+ "average": "Average",
+ "good": "Good",
+ "veryGood": "Very good",
+ "excellent": "Excellent"
+ },
+ "copyAddress": "Copy shop address",
+ "addressCopied": "Address copied",
+ "sendMessage": "Send message",
+ "contactInfoButton": "Contact info",
+ "followButton": "Follow",
+ "unfollowButton": "Unfollow",
+ "lessThanOneMonth": "Less than 1 month",
+ "monthsCount": "{{count}} months",
+ "showLocation": "Show location",
+ "shopFollowSuccess": "Shop followed",
+ "shopUnfollowSuccess": "Unfollowed",
+ "productDeactivated": "Product deactivated",
+ "productActivated": "Product activated",
+ "directManagement": "Direct management",
+ "itemsCount": "{{count}} items",
+ "activeProductsLabel": "{{count}} active",
+ "soldCountLabel": "{{count}} sold",
+ "inStock": "In stock",
+ "outOfStock": "Out of stock",
+ "ordersTitle": "Shop orders",
"sellerTab": "Seller",
"buyerTab": "Buyer",
"sellerOrdersEmpty": "No orders for your shop yet",
@@ -2229,6 +2321,7 @@
"status": {
"draft": "Draft",
"pending_review": "Pending review",
+ "pending_shop_approval": "Pending approval",
"active": "Active",
"inactive": "Inactive",
"rejected": "Rejected"
@@ -2237,9 +2330,14 @@
"productNamePlaceholder": "Enter product name",
"productNameRequired": "Product name is required",
"productDescriptionPlaceholder": "Product description (optional)",
+ "productCategoryTitle": "Product category",
+ "productCategoryRequired": "Selecting a main category, subcategory, and tertiary category is required",
+ "selectMainCategory": "Select the main category",
+ "selectSubSubCategory": "Select the tertiary category",
"productImagesTitle": "Product images",
"productImagesHint": "Add multiple images and pick one as the primary image",
"primaryImage": "Primary image",
+ "removeImage": "Remove image",
"setPrimaryImage": "Set as primary",
"variantsTitle": "Price & specifications",
"colorsPlaceholder": "Colors, comma-separated (e.g. red, blue)",
@@ -2247,8 +2345,17 @@
"weightsPlaceholder": "Weights, comma-separated (optional)",
"generateVariants": "Build price table",
"defaultVariant": "Default",
- "pricePlaceholder": "Price (Toman)",
+ "pricePlaceholder": "Original price",
+ "discountPricePlaceholder": "Discounted price",
"stockPlaceholder": "Stock",
+ "discountPercentLabel": "{{percent}}% off",
+ "discountPercentBadge": "{{percent}}%",
+ "colorColumn": "Color",
+ "sizeColumn": "Size",
+ "weightColumn": "Weight",
+ "priceColumn": "Price (Toman)",
+ "totalVariantsLabel": "Total variants: {{count}}",
+ "removeVariant": "Remove",
"variantsRequired": "Enter at least one combination with a valid price",
"saveProduct": "Save product",
"productSaved": "Product saved successfully",
@@ -2308,6 +2415,30 @@
"returnReasonPlaceholder": "Enter the reason for the return",
"submitReturn": "Submit return request",
"returnRequestSubmitted": "Return request submitted",
+ "yourRatingLabel": "Your rating",
+ "buyerMobileLabel": "Mobile:",
+ "shopInfoTitle": "Shop info",
+ "orderNumberLabel": "Order number",
+ "searchOrderPlaceholder": "Search order number",
+ "provinceRequired": "Province is required",
+ "cityRequired": "City is required",
+ "viewShopProfileButton": "View shop profile",
+ "shippingTimeLabel": "Shipping time",
+ "shippingTypeLabel": "Shipping type",
+ "shippingCostLabel": "Shipping cost",
+ "grandTotalLabel": "Grand total",
+ "reportStatusTitle": "Reported issue",
+ "returnStatusTitle": "Return request",
+ "reportStatus": {
+ "open": "Under review",
+ "resolved": "Resolved"
+ },
+ "returnStatus": {
+ "pending": "Pending review",
+ "approved": "Approved",
+ "rejected": "Rejected",
+ "refunded": "Refunded"
+ },
"withdrawalRequested": "Withdrawal request submitted successfully",
"totalBalance": "Total wallet balance",
"walletShopTab": "Shop",
diff --git a/src/locales/fa/common.json b/src/locales/fa/common.json
index 6bc8d23..8eaba39 100644
--- a/src/locales/fa/common.json
+++ b/src/locales/fa/common.json
@@ -2173,11 +2173,23 @@
"loadingCategories": "در حال بارگذاری دستهبندیها...",
"selectSubCategory": "زیردستههای خود را انتخاب کنید",
"subCategoryDisplayHint": "تیک کنار هر مورد، نمایش آن در صفحه فروشگاه است",
+ "subCategoryMaxHint": "حداکثر ۳ زیردسته قابل انتخاب است",
+ "subCategoryMaxReached": "حداکثر ۳ زیردسته میتوانید انتخاب کنید",
"displaySubCategoryAria": "نمایش {{name}} در فروشگاه",
"unknownError": "خطای نامشخصی رخ داد",
"saveAndContinue": "ثبت و ادامه",
"nameTitle": "نام فروشگاه",
"namePlaceholder": "نام فروشگاه را وارد کنید",
+ "descriptionPlaceholder": "توضیحات فروشگاه (اختیاری)",
+ "share": {
+ "title": "اشتراکگذاری فروشگاه",
+ "linkLabel": "لینک فروشگاه",
+ "copyLink": "کپی لینک",
+ "copied": "لینک کپی شد",
+ "copyFailed": "کپی لینک انجام نشد",
+ "qrAlt": "بارکد فروشگاه",
+ "close": "بستن"
+ },
"nameRequired": "نام فروشگاه الزامی است",
"logoTitle": "لوگوی فروشگاه",
"selectLogo": "انتخاب/ویرایش لوگو",
@@ -2186,6 +2198,8 @@
"categoryRequired": "انتخاب دستهبندی، زیردسته و مورد نمایشی الزامی است",
"shippingTitle": "روشهای ارسال",
"shippingMethodRequired": "حداقل یک روش ارسال را انتخاب کنید",
+ "activeShippingMethods": "لیست روشهای ارسال فعال",
+ "supplementarySettings": "تنظیمات مکمل و خدمات",
"shippingMethods": {
"post": "پست",
"tipax": "تیپاکس",
@@ -2204,9 +2218,11 @@
"neighbourhoodPlaceholder": "محله (اختیاری)",
"addressPlaceholder": "آدرس (اختیاری)",
"contactTitle": "اطلاعات پشتیبانی فروشگاه",
- "phonePlaceholder": "تلفن",
+ "landlinePlaceholder": "تلفن ثابت",
"contactRequired": "حداقل یک راه ارتباطی وارد کنید",
"responseScheduleTitle": "روزها و ساعات پاسخگویی",
+ "responseStartTime": "ساعت شروع",
+ "responseEndTime": "ساعت پایان",
"days": {
"sat": "شنبه",
"sun": "یکشنبه",
@@ -2217,11 +2233,87 @@
"fri": "جمعه"
},
"previewTitle": "پیشنمایش فروشگاه",
+ "previewMinOrder": "حداقل سفارش",
+ "previewDeliveryTime": "زمان ارسال",
+ "previewYes": "دارم",
"submitShop": "ثبت فروشگاه",
"submitSuccess": "فروشگاه با موفقیت برای بررسی ارسال شد",
"mySwitcher": "فروشگاههای من",
"addShop": "افزودن فروشگاه/کالا",
"noShopsYet": "هنوز فروشگاهی ثبت نکردهاید",
+ "myShopsTitle": "لیست فروشگاههای من",
+ "myProductsTitle": "محصولات من",
+ "addShopNew": "افزودن فروشگاه جدید",
+ "addProductNew": "افزودن کالا",
+ "searchShopPlaceholder": "جستجوی فروشگاه",
+ "searchProductPlaceholder": "جستجوی محصول",
+ "onlineShop": "فروشگاه اینترنتی",
+ "directManagement": "مدیریت مستقیم",
+ "itemsCount": "{{count}} کالا",
+ "activeProductsLabel": "{{count}} کالای فعال",
+ "soldCountLabel": "{{count}} فروخته شده",
+ "inStock": "موجود",
+ "outOfStock": "اتمام موجودی",
+ "inPerson": "حضوری",
+ "onlineOnly": "اینترنتی",
+ "categoryLabel": "دستهبندی: {{category}}",
+ "stockCountLabel": "{{count}} عدد",
+ "specsTitle": "مشخصات کالا",
+ "categorySpecLabel": "دستهبندی",
+ "sizeLabel": "سایز",
+ "colorLabel": "رنگ",
+ "weightLabel": "وزن",
+ "descriptionLabel": "توضیحات محصول",
+ "priceSpecLabel": "قیمت",
+ "showMore": "بیشتر",
+ "showLess": "کمتر",
+ "manageStockButton": "مدیریت موجودی کالا",
+ "deactivateButton": "غیرفعال کردن",
+ "activateButton": "فعالسازی",
+ "editProductButton": "ویرایش اطلاعات کالا",
+ "editShopButton": "ویرایش فروشگاه",
+ "viewPublicProfile": "نمایش صفحه فروشگاه",
+ "viewProduct": "مشاهده کالا",
+ "myOrdersFromShop": "پیگیری سفارشات من",
+ "noOrdersFromShop": "هنوز سفارشی از این فروشگاه ثبت نکردهاید",
+ "satisfactionLabel": "رضایت از کالاها",
+ "performanceLabel": "عملکرد فروشگاه",
+ "noRatingYet": "بدون امتیاز",
+ "productsLabel": "کالا",
+ "underReview": "در حال بررسی",
+ "newShop": "فروشگاه جدید",
+ "placeOrderButton": "ثبت خرید",
+ "addToCartButton": "افزودن به سبد خرید",
+ "addToCartSuccess": "به سبد خرید اضافه شد",
+ "addToFavoritesButton": "افزودن به علاقمندیها",
+ "removeFromFavoritesButton": "حذف از علاقمندیها",
+ "addedToFavorites": "به علاقمندیها اضافه شد",
+ "removedFromFavorites": "از علاقمندیها حذف شد",
+ "sendProductTitle": "ارسال کالا",
+ "sentProductToCount": "کالا به {{count}} نفر ارسال شد",
+ "sendProductError": "خطا در ارسال کالا",
+ "noChatMessagesYet": "هنوز پیامی رد و بدل نشده است",
+ "performance": {
+ "weak": "ضعیف",
+ "average": "معمولی",
+ "good": "خوب",
+ "veryGood": "بسیار خوب",
+ "excellent": "عالی"
+ },
+ "copyAddress": "کپی آدرس فروشگاه",
+ "addressCopied": "آدرس کپی شد",
+ "sendMessage": "ارسال پیام",
+ "contactInfoButton": "اطلاعات تماس",
+ "followButton": "دنبال کردن",
+ "unfollowButton": "لغو دنبال کردن",
+ "lessThanOneMonth": "کمتر از ۱ ماه",
+ "monthsCount": "{{count}} ماه",
+ "showLocation": "نمایش لوکیشن",
+ "shopFollowSuccess": "فروشگاه دنبال شد",
+ "shopUnfollowSuccess": "دنبال کردن لغو شد",
+ "productDeactivated": "کالا غیرفعال شد",
+ "productActivated": "کالا فعال شد",
+ "ordersTitle": "سفارشهای فروشگاه",
"sellerTab": "فروشنده",
"buyerTab": "خریدار",
"sellerOrdersEmpty": "هنوز سفارشی برای فروشگاه شما ثبت نشده است",
@@ -2229,6 +2321,7 @@
"status": {
"draft": "پیشنویس",
"pending_review": "در انتظار بررسی",
+ "pending_shop_approval": "در انتظار تایید",
"active": "فعال",
"inactive": "غیرفعال",
"rejected": "رد شده"
@@ -2237,9 +2330,14 @@
"productNamePlaceholder": "نام کالا را وارد کنید",
"productNameRequired": "نام کالا الزامی است",
"productDescriptionPlaceholder": "توضیحات کالا (اختیاری)",
+ "productCategoryTitle": "دستهبندی کالا",
+ "productCategoryRequired": "انتخاب دستهبندی اصلی، زیرگروه و دستهبندی فرعی الزامی است",
+ "selectMainCategory": "دستهبندی اصلی را انتخاب کنید",
+ "selectSubSubCategory": "دستهبندی فرعی را انتخاب کنید",
"productImagesTitle": "تصاویر کالا",
"productImagesHint": "میتوانید چند تصویر اضافه کنید و یکی را بهعنوان تصویر اصلی انتخاب کنید",
"primaryImage": "تصویر اصلی",
+ "removeImage": "حذف تصویر",
"setPrimaryImage": "انتخاب بهعنوان اصلی",
"variantsTitle": "قیمت و مشخصات کالا",
"colorsPlaceholder": "رنگها را با کاما جدا کنید (مثلاً قرمز, آبی)",
@@ -2247,8 +2345,17 @@
"weightsPlaceholder": "وزنها را با کاما جدا کنید (اختیاری)",
"generateVariants": "ساخت جدول قیمت",
"defaultVariant": "پیشفرض",
- "pricePlaceholder": "قیمت (تومان)",
+ "pricePlaceholder": "قیمت اصلی",
+ "discountPricePlaceholder": "قیمت با تخفیف",
"stockPlaceholder": "موجودی",
+ "discountPercentLabel": "{{percent}}% تخفیف",
+ "discountPercentBadge": "{{percent}}٪",
+ "colorColumn": "رنگ",
+ "sizeColumn": "سایز",
+ "weightColumn": "وزن",
+ "priceColumn": "قیمت (تومان)",
+ "totalVariantsLabel": "تعداد کل تنوع کالا: {{count}}",
+ "removeVariant": "حذف",
"variantsRequired": "حداقل یک ترکیب با قیمت معتبر وارد کنید",
"saveProduct": "ثبت کالا",
"productSaved": "کالا با موفقیت ثبت شد",
@@ -2308,6 +2415,30 @@
"returnReasonPlaceholder": "دلیل درخواست مرجوعی را وارد کنید",
"submitReturn": "ثبت درخواست مرجوعی",
"returnRequestSubmitted": "درخواست مرجوعی با موفقیت ثبت شد",
+ "yourRatingLabel": "امتیاز شما",
+ "buyerMobileLabel": "شماره موبایل:",
+ "shopInfoTitle": "اطلاعات فروشگاه",
+ "orderNumberLabel": "شماره سفارش",
+ "searchOrderPlaceholder": "جستجوی شماره سفارش",
+ "provinceRequired": "انتخاب استان الزامی است",
+ "cityRequired": "انتخاب شهر الزامی است",
+ "viewShopProfileButton": "مشاهده پروفایل فروشگاه",
+ "shippingTimeLabel": "زمان ارسال",
+ "shippingTypeLabel": "نوع ارسال",
+ "shippingCostLabel": "هزینه ارسال",
+ "grandTotalLabel": "جمع کل",
+ "reportStatusTitle": "مشکل ثبتشده",
+ "returnStatusTitle": "درخواست مرجوعی",
+ "reportStatus": {
+ "open": "در حال بررسی",
+ "resolved": "بررسی شده"
+ },
+ "returnStatus": {
+ "pending": "در انتظار بررسی",
+ "approved": "تایید شده",
+ "rejected": "رد شده",
+ "refunded": "مسترد شده"
+ },
"withdrawalRequested": "درخواست تسویه با موفقیت ثبت شد",
"totalBalance": "موجودی کل کیف پول",
"walletShopTab": "فروشگاه",
diff --git a/src/types/types.ts b/src/types/types.ts
index fb27df9..247a6b2 100644
--- a/src/types/types.ts
+++ b/src/types/types.ts
@@ -93,6 +93,12 @@ export interface IShopCategory {
export interface ISubShopCategory {
name: string;
_id: string;
+ sub_sub_categories?: ISubSubShopCategory[];
+}
+
+export interface ISubSubShopCategory {
+ name: string;
+ _id: string;
}
export interface IShopProductVariant {
@@ -112,6 +118,9 @@ export interface IShopProductListing {
catalogProduct: string | { _id: string; name: string; description?: string | null };
title: string;
description?: string | null;
+ category?: string | null;
+ sub_category?: string | null;
+ sub_sub_category?: string | null;
images: string[];
primaryImageIndex: number;
variants: IShopProductVariant[];