Fix shop UI RTL/layout consistency and tab reselect behavior
- Align shop header layout (logo, name, verification badge, share button) and RTL/LTR positioning across shop, listing, billboard, wallet, gift, and academy pages with the Android app's equivalent screens - Fix TabNavigation's tab-reselect scroll-to-top to only fire on the exact tab route, not any active sub-route - Add verification badge to shop owner name; add brand icon assets Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1
public/images/icons/brand/instagram-brand.svg
Normal file
1
public/images/icons/brand/instagram-brand.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 102 102" id="instagram"><defs><radialGradient id="a" cx="6.601" cy="99.766" r="129.502" gradientUnits="userSpaceOnUse"><stop offset=".09" stop-color="#fa8f21"></stop><stop offset=".78" stop-color="#d82d7e"></stop></radialGradient><radialGradient id="b" cx="70.652" cy="96.49" r="113.963" gradientUnits="userSpaceOnUse"><stop offset=".64" stop-color="#8c3aaa" stop-opacity="0"></stop><stop offset="1" stop-color="#8c3aaa"></stop></radialGradient></defs><path fill="url(#a)" d="M25.865,101.639A34.341,34.341,0,0,1,14.312,99.5a19.329,19.329,0,0,1-7.154-4.653A19.181,19.181,0,0,1,2.5,87.694,34.341,34.341,0,0,1,.364,76.142C.061,69.584,0,67.617,0,51s.067-18.577.361-25.14A34.534,34.534,0,0,1,2.5,14.312,19.4,19.4,0,0,1,7.154,7.154,19.206,19.206,0,0,1,14.309,2.5,34.341,34.341,0,0,1,25.862.361C32.422.061,34.392,0,51,0s18.577.067,25.14.361A34.534,34.534,0,0,1,87.691,2.5a19.254,19.254,0,0,1,7.154,4.653A19.267,19.267,0,0,1,99.5,14.309a34.341,34.341,0,0,1,2.14,11.553c.3,6.563.361,8.528.361,25.14s-.061,18.577-.361,25.14A34.5,34.5,0,0,1,99.5,87.694,20.6,20.6,0,0,1,87.691,99.5a34.342,34.342,0,0,1-11.553,2.14c-6.557.3-8.528.361-25.14.361s-18.577-.058-25.134-.361"></path><path fill="url(#b)" d="M25.865,101.639A34.341,34.341,0,0,1,14.312,99.5a19.329,19.329,0,0,1-7.154-4.653A19.181,19.181,0,0,1,2.5,87.694,34.341,34.341,0,0,1,.364,76.142C.061,69.584,0,67.617,0,51s.067-18.577.361-25.14A34.534,34.534,0,0,1,2.5,14.312,19.4,19.4,0,0,1,7.154,7.154,19.206,19.206,0,0,1,14.309,2.5,34.341,34.341,0,0,1,25.862.361C32.422.061,34.392,0,51,0s18.577.067,25.14.361A34.534,34.534,0,0,1,87.691,2.5a19.254,19.254,0,0,1,7.154,4.653A19.267,19.267,0,0,1,99.5,14.309a34.341,34.341,0,0,1,2.14,11.553c.3,6.563.361,8.528.361,25.14s-.061,18.577-.361,25.14A34.5,34.5,0,0,1,99.5,87.694,20.6,20.6,0,0,1,87.691,99.5a34.342,34.342,0,0,1-11.553,2.14c-6.557.3-8.528.361-25.14.361s-18.577-.058-25.134-.361"></path><path fill="#fff" d="M461.114,477.413a12.631,12.631,0,1,1,12.629,12.632,12.631,12.631,0,0,1-12.629-12.632m-6.829,0a19.458,19.458,0,1,0,19.458-19.458,19.457,19.457,0,0,0-19.458,19.458m35.139-20.229a4.547,4.547,0,1,0,4.549-4.545h0a4.549,4.549,0,0,0-4.547,4.545m-30.99,51.074a20.943,20.943,0,0,1-7.037-1.3,12.547,12.547,0,0,1-7.193-7.19,20.923,20.923,0,0,1-1.3-7.037c-.184-3.994-.22-5.194-.22-15.313s.04-11.316.22-15.314a21.082,21.082,0,0,1,1.3-7.037,12.54,12.54,0,0,1,7.193-7.193,20.924,20.924,0,0,1,7.037-1.3c3.994-.184,5.194-.22,15.309-.22s11.316.039,15.314.221a21.082,21.082,0,0,1,7.037,1.3,12.541,12.541,0,0,1,7.193,7.193,20.926,20.926,0,0,1,1.3,7.037c.184,4,.22,5.194.22,15.314s-.037,11.316-.22,15.314a21.023,21.023,0,0,1-1.3,7.037,12.547,12.547,0,0,1-7.193,7.19,20.925,20.925,0,0,1-7.037,1.3c-3.994.184-5.194.22-15.314.22s-11.316-.037-15.309-.22m-.314-68.509a27.786,27.786,0,0,0-9.2,1.76,19.373,19.373,0,0,0-11.083,11.083,27.794,27.794,0,0,0-1.76,9.2c-.187,4.04-.229,5.332-.229,15.623s.043,11.582.229,15.623a27.793,27.793,0,0,0,1.76,9.2,19.374,19.374,0,0,0,11.083,11.083,27.813,27.813,0,0,0,9.2,1.76c4.042.184,5.332.229,15.623.229s11.582-.043,15.623-.229a27.8,27.8,0,0,0,9.2-1.76,19.374,19.374,0,0,0,11.083-11.083,27.716,27.716,0,0,0,1.76-9.2c.184-4.043.226-5.332.226-15.623s-.043-11.582-.226-15.623a27.786,27.786,0,0,0-1.76-9.2,19.379,19.379,0,0,0-11.08-11.083,27.748,27.748,0,0,0-9.2-1.76c-4.041-.185-5.332-.229-15.621-.229s-11.583.043-15.626.229" transform="translate(-422.637 -426.196)"></path></svg>
|
||||
|
After Width: | Height: | Size: 3.4 KiB |
14
public/images/icons/brand/phone-brand.svg
Normal file
14
public/images/icons/brand/phone-brand.svg
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#00FF00;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
</style>
|
||||
<path class="st0" d="M9.3,48h29.3c5.2,0,9.3-4.2,9.3-9.3V9.3C48,4.2,43.8,0,38.7,0H9.3C4.2,0,0,4.2,0,9.3v29.3C0,43.8,4.2,48,9.3,48
|
||||
z"/>
|
||||
<path class="st1" d="M39.3,33.4l-6.1-4.1c-0.8-0.5-1.7-0.5-2.4,0c0,0,0,0-2.6,1.5c-0.4,0.2-0.7,0.2-1,0.2c-0.3-0.1-0.6-0.3-0.6-0.3
|
||||
c-1.4-1.1-3.1-2.5-4.9-4.3c-1.8-1.8-3.2-3.5-4.3-4.9c0,0-0.2-0.3-0.3-0.6c0-0.3,0-0.6,0.2-1c1.5-2.6,1.5-2.6,1.5-2.6
|
||||
c0.4-0.8,0.4-1.7,0-2.4l-4.1-6.1c-0.5-0.8-1.5-1-2.3-0.4c0,0-1.9,1.3-2.6,1.8c-1,0.7-1.3,1.8-1.3,3.3c0,2.1,1.8,8.8,9.6,16.6l0,0
|
||||
l0,0l0,0l0,0c7.8,7.8,14.5,9.6,16.6,9.6c1.5,0,2.6-0.3,3.3-1.3c0.4-0.6,1.8-2.6,1.8-2.6C40.2,34.9,40,33.9,39.3,33.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 953 B |
16
public/images/icons/brand/telegram-brand.svg
Normal file
16
public/images/icons/brand/telegram-brand.svg
Normal file
@@ -0,0 +1,16 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:#29B6F6;}
|
||||
.st1{fill:#FFFFFF;}
|
||||
.st2{fill:#B0BEC5;}
|
||||
.st3{fill:#CFD8DC;}
|
||||
</style>
|
||||
<path class="st0" d="M24,0C10.7,0,0,10.7,0,24s10.7,24,24,24s24-10.7,24-24S37.3,0,24,0z"/>
|
||||
<path class="st1" d="M35.9,13.2l-4.5,23c0,0-0.2,1-1.5,1c-0.7,0-1-0.3-1-0.3l-9.7-8.1l-4.8-2.4l-6.1-1.6c0,0-1.1-0.3-1.1-1.2
|
||||
c0-0.8,1.1-1.1,1.1-1.1l25.6-10.2c0,0,0.8-0.3,1.4-0.3c0.4,0,0.8,0.1,0.8,0.6C36,12.9,35.9,13.2,35.9,13.2z"/>
|
||||
<path class="st2" d="M22.8,31.8l-4.1,4c0,0-0.2,0.1-0.4,0.1c-0.1,0-0.2,0-0.3-0.1l1.2-7.2L22.8,31.8z"/>
|
||||
<path class="st3" d="M31.1,17c-0.2-0.3-0.6-0.3-0.8-0.1l-15.8,9.5c0,0,2.5,7.1,2.9,8.3c0.4,1.2,0.7,1.3,0.7,1.3l1.2-7.2L31,17.9
|
||||
C31.2,17.7,31.3,17.3,31.1,17z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 955 B |
26
public/images/icons/brand/whatsapp-brand.svg
Normal file
26
public/images/icons/brand/whatsapp-brand.svg
Normal file
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 48 48" style="enable-background:new 0 0 48 48;" xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill-rule:evenodd;clip-rule:evenodd;fill:#FFFFFF;}
|
||||
.st1{fill-rule:evenodd;clip-rule:evenodd;fill:#CFD8DC;}
|
||||
.st2{fill-rule:evenodd;clip-rule:evenodd;fill:#40C351;}
|
||||
</style>
|
||||
<path class="st0" d="M1.3,46.8l3.2-11.7c-2-3.4-3-7.3-3-11.3c0-12.4,10.1-22.5,22.6-22.5c6,0,11.7,2.4,16,6.6
|
||||
c4.3,4.3,6.6,9.9,6.6,16c0,12.4-10.1,22.5-22.6,22.5c0,0,0,0,0,0h0c-3.8,0-7.5-0.9-10.8-2.7L1.3,46.8z"/>
|
||||
<path class="st0" d="M1.3,47.4c-0.2,0-0.3-0.1-0.4-0.2c-0.1-0.2-0.2-0.4-0.2-0.6l3.1-11.4c-1.9-3.5-3-7.4-3-11.4
|
||||
c0-12.8,10.4-23.1,23.2-23.1c6.2,0,12,2.4,16.4,6.8c4.4,4.4,6.8,10.2,6.8,16.4C47.2,36.6,36.9,47,24.1,47c-3.8,0-7.5-0.9-10.9-2.7
|
||||
L1.5,47.4C1.4,47.4,1.4,47.4,1.3,47.4z"/>
|
||||
<path class="st1" d="M24.1,1.3c6,0,11.7,2.4,16,6.6c4.3,4.3,6.6,9.9,6.6,16c0,12.4-10.1,22.5-22.6,22.5h0c-3.8,0-7.5-0.9-10.8-2.7
|
||||
l-12,3.1l3.2-11.7c-2-3.4-3-7.3-3-11.3C1.5,11.4,11.7,1.3,24.1,1.3 M24.1,46.4L24.1,46.4L24.1,46.4 M24.1,46.4L24.1,46.4L24.1,46.4
|
||||
M24.1,0.1L24.1,0.1C11,0.1,0.4,10.8,0.3,23.9c0,4,1,7.9,2.9,11.4L0.2,46.5c-0.1,0.4,0,0.8,0.3,1.1C0.7,47.9,1,48,1.3,48
|
||||
c0.1,0,0.2,0,0.3,0l11.5-3c3.4,1.7,7.1,2.7,10.9,2.7c13.1,0,23.7-10.6,23.8-23.7c0-6.3-2.5-12.3-6.9-16.8
|
||||
C36.4,2.6,30.4,0.1,24.1,0.1L24.1,0.1z"/>
|
||||
<path class="st2" d="M37.4,10.6c-3.5-3.5-8.2-5.5-13.3-5.5c-10.3,0-18.8,8.4-18.8,18.7c0,3.5,1,7,2.9,10l0.4,0.7l-1.9,6.9l7.1-1.9
|
||||
l0.7,0.4c2.9,1.7,6.2,2.6,9.5,2.6h0c10.3,0,18.7-8.4,18.7-18.7C42.8,18.9,40.9,14.2,37.4,10.6z"/>
|
||||
<path class="st0" d="M18.5,14.4c-0.4-0.9-0.9-1-1.3-1c-0.3,0-0.7,0-1.1,0c-0.4,0-1,0.1-1.5,0.7c-0.5,0.6-2,1.9-2,4.7s2,5.5,2.3,5.8
|
||||
s3.9,6.2,9.6,8.5c4.8,1.9,5.7,1.5,6.8,1.4c1-0.1,3.3-1.4,3.8-2.7c0.5-1.3,0.5-2.4,0.3-2.7c-0.1-0.2-0.5-0.4-1.1-0.7
|
||||
c-0.6-0.3-3.3-1.6-3.9-1.8s-0.9-0.3-1.3,0.3c-0.4,0.6-1.5,1.8-1.8,2.2c-0.3,0.4-0.7,0.4-1.2,0.1c-0.6-0.3-2.4-0.9-4.5-2.8
|
||||
c-1.7-1.5-2.8-3.3-3.1-3.9c-0.3-0.6,0-0.9,0.2-1.2c0.3-0.3,0.6-0.7,0.8-1c0.3-0.3,0.4-0.6,0.6-0.9c0.2-0.4,0.1-0.7,0-1
|
||||
C20.1,18.3,19,15.5,18.5,14.4z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.1 KiB |
@@ -524,9 +524,11 @@ export default function CourseDetail({
|
||||
}
|
||||
|
||||
if (paymentUrl) {
|
||||
window.open(paymentUrl, "_blank", "noopener,noreferrer");
|
||||
toast.success(t("academy.course.redirectedToPayment"));
|
||||
|
||||
// window.open بعدِ یه await در سافاری (دسکتاپ و خصوصاً iOS) پاپآپبلاک میشه —
|
||||
// چون دیگه synchronously به همون کلیکِ کاربر متصل نیست. window.location.href هم
|
||||
// مطمئنتره، هم با بقیهی مسیرهای پرداختِ این اپ (booking، پروژه، بیلبورد) یکی میمونه
|
||||
window.location.href = paymentUrl;
|
||||
return;
|
||||
} else {
|
||||
console.error("ساختار پاسخ نامعتبر:", response);
|
||||
toast.error(
|
||||
|
||||
@@ -82,7 +82,9 @@ function FailedProject() {
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -66,7 +66,9 @@ function BillboardPayment({ params }: IBillboardProps) {
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -56,7 +56,9 @@ function FailedBillboard() {
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
@@ -80,7 +82,9 @@ function FailedBillboard() {
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -9,7 +9,6 @@ import useAxios from "@/hooks/useAxios";
|
||||
import { filterChipClass } from "@/lib/ui/buttonStyles";
|
||||
import { User } from "@/types/types";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -25,7 +24,6 @@ function GiftPage({ params }: GiftPageProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { userId } = React.use(params);
|
||||
const { request } = useAxios();
|
||||
const router = useRouter();
|
||||
|
||||
const [userDetail, setUserDetail] = useState<User>();
|
||||
const [selectedAmount, setSelectedAmount] = useState<number | null>(
|
||||
@@ -67,10 +65,12 @@ function GiftPage({ params }: GiftPageProps) {
|
||||
"/gifts/initiate",
|
||||
{ receiverId: userId, amount: finalAmount, message }
|
||||
);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
if (response?.paymentUrl) {
|
||||
router.push(response.paymentUrl);
|
||||
window.location.href = response.paymentUrl;
|
||||
} else if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
} else {
|
||||
toast.error(t("giftPage.paymentInfoError"));
|
||||
}
|
||||
|
||||
@@ -158,13 +158,14 @@ function TicketChat({ params }: ITicketChatProps) {
|
||||
setHasUsedFreeOffer(true);
|
||||
router.push(`/offer/payment/success?userId=${id}`);
|
||||
} else if (data.paymentUrl) {
|
||||
window.open(data.paymentUrl, "_blank", "noopener,noreferrer");
|
||||
// window.open بعدِ await در سافاری (دسکتاپ/iOS) پاپآپبلاک میشه چون دیگه
|
||||
// synchronously به کلیکِ کاربر متصل نیست؛ ضمناً بازگشتِ ZarinPal (از طریق
|
||||
// offer/payment-web/route.js) کلِ همون تب رو به "/" ریدایرکت میکنه، نه تبِ
|
||||
// اصلی — پس با _blank دو تب باز میموند. window.location.href هم امنتره
|
||||
// هم با بقیهی مسیرهای پرداختِ این اپ یکی میمونه
|
||||
window.location.href = data.paymentUrl;
|
||||
} else if (data.authority) {
|
||||
window.open(
|
||||
`https://www.zarinpal.com/pg/StartPay/${data.authority}`,
|
||||
"_blank",
|
||||
"noopener,noreferrer"
|
||||
);
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${data.authority}`;
|
||||
} else {
|
||||
toast.error(t("offerPage.paymentInfoError"));
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ import Header from "@/components/main/Header";
|
||||
import ShopListCard from "@/components/shops/ShopListCard";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
type MyShop = {
|
||||
@@ -28,15 +29,19 @@ export default function ShopSettingsPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const { request } = useAxios();
|
||||
const [myShops, setMyShops] = useState<MyShop[] | null>(null);
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
request<{ shops: MyShop[] }>("GET", "/shops/mine")
|
||||
.then((res) => setMyShops(res?.shops || []))
|
||||
.catch(() => setMyShops([]));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
// طبق خواستهی کاربر: بعدِ برگشت از ویرایشِ فروشگاه بدونِ رفرشِ دستی تغییرات دیده
|
||||
// بشن — قبلاً با useEffect(..., []) فقط یهبار موقعِ اولین mount فچ میشد؛ عینِ
|
||||
// useInfiniteScroll (لیستِ کالاها) با refetchOnMount:"always" هر بار برگردیم به
|
||||
// این صفحه دوباره فچ میشه
|
||||
const { data } = useQuery({
|
||||
queryKey: ["shops-mine"],
|
||||
queryFn: () => request<{ shops: MyShop[] }>("GET", "/shops/mine"),
|
||||
staleTime: 0,
|
||||
refetchOnMount: "always",
|
||||
});
|
||||
const myShops = data?.shops ?? null;
|
||||
|
||||
const filteredShops = useMemo(() => {
|
||||
if (!myShops) return [];
|
||||
|
||||
@@ -8,7 +8,7 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { toggleBtnClass } from "@/lib/ui/buttonStyles";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -31,7 +31,6 @@ type WalletSummary = {
|
||||
|
||||
export default function WalletSettingsPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { request, loading } = useAxios();
|
||||
|
||||
@@ -88,7 +87,9 @@ export default function WalletSettingsPage() {
|
||||
{ amount }
|
||||
);
|
||||
if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
|
||||
@@ -8,6 +8,7 @@ import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
|
||||
import ShareShopModal from "@/components/shops/ShareShopModal";
|
||||
import ShopProductListRow from "@/components/shops/ShopProductListRow";
|
||||
import PageTitle from "@/components/settings/PageTitle";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -25,10 +26,12 @@ type ShopHeader = {
|
||||
_id: string;
|
||||
name: string;
|
||||
logo?: string | null;
|
||||
owner?: { verify_badge?: string | null } | null;
|
||||
};
|
||||
|
||||
export default function ShopProductsPage() {
|
||||
const { t } = useTranslation("common");
|
||||
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;
|
||||
@@ -73,43 +76,55 @@ export default function ShopProductsPage() {
|
||||
<Header />
|
||||
<LocalePageShell>
|
||||
<Container className="pb-28">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
aria-label={t("shops.share.title")}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center"
|
||||
{/* طبق خواستهی کاربر: لوگو راست (فرزند اول → فیزیکی راست زیرِ RTL)، بعدش نامِ
|
||||
فروشگاه (فرزند دوم، flex-1 باقی فضا رو پر میکنه و کلیکپذیره مثلِ لوگو)،
|
||||
و اشتراکگذاری سمتِ چپِ کادر و روبروی نام (فرزند سوم/آخر → فیزیکی چپترین) —
|
||||
عینِ اندروید (ShopProductsScreen.tsx) */}
|
||||
<div className="flex items-center gap-2">
|
||||
{shopId && (
|
||||
<Link
|
||||
href={`/shops/profile/${shopId}`}
|
||||
aria-label={t("shops.viewPublicProfile")}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800"
|
||||
>
|
||||
<BoldIcon name="share" size={18} tinted />
|
||||
</button>
|
||||
{shopId && (
|
||||
<Link
|
||||
href={`/shops/profile/${shopId}`}
|
||||
aria-label={t("shops.viewPublicProfile")}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800"
|
||||
>
|
||||
{shop?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shop.logo}
|
||||
alt={shop.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{shop?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shop.logo}
|
||||
alt={shop.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={16}
|
||||
height={16}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
<span className="truncate text-sm font-semibold">{shop?.name}</span>
|
||||
<Link
|
||||
href={`/shops/profile/${shopId}`}
|
||||
className={cn(
|
||||
"flex flex-1 items-center gap-1 truncate text-sm font-semibold",
|
||||
isFa ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
{shop?.name}
|
||||
<VerificationBadge verifyBadge={shop?.owner?.verify_badge} size={13} />
|
||||
</Link>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowShareModal(true)}
|
||||
aria-label={t("shops.share.title")}
|
||||
className="flex h-8 w-8 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="share" size={18} tinted />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<PageTitle>{t("shops.myProductsTitle")}</PageTitle>
|
||||
|
||||
@@ -11,6 +11,7 @@ import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { saveProductWizardId } from "@/hooks/useProductWizardId";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { copyTextToClipboard } from "@/lib/chat/getCopyableMessageText";
|
||||
import { IShopProductListing, IShopProductVariant } from "@/types/types";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import Image from "next/image";
|
||||
@@ -197,6 +198,23 @@ export default function ProductManagePage() {
|
||||
router.push(`/shops/${shopId}/products/new/name?edit=1`);
|
||||
};
|
||||
|
||||
// طبق خواستهی کاربر: اشتراکگذاری لینکِ خودِ کالا — عینِ ListingDetailClient.tsx
|
||||
const shareProduct = async () => {
|
||||
if (!listing?._id) return;
|
||||
const productUrl = `https://modstagram.com/shops/listing/${listing._id}`;
|
||||
if (typeof navigator !== "undefined" && navigator.share) {
|
||||
try {
|
||||
await navigator.share({ url: productUrl, title: listing.title });
|
||||
return;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ok = await copyTextToClipboard(productUrl);
|
||||
if (ok) toast.success(t("shops.share.copied"));
|
||||
else toast.error(t("shops.share.copyFailed"));
|
||||
};
|
||||
|
||||
if (!listing) return null;
|
||||
|
||||
const images = listing.images || [];
|
||||
@@ -213,33 +231,52 @@ export default function ProductManagePage() {
|
||||
<Header />
|
||||
<LocalePageShell>
|
||||
<Container className="pb-28">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="flex w-full">
|
||||
<BackButton />
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
|
||||
{shopInfo?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shopInfo.logo}
|
||||
alt={shopInfo.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={18}
|
||||
height={18}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mt-4 flex w-full max-w-md flex-col gap-4 rounded-3xl border border-neutral-200 p-4 dark:border-neutral-700">
|
||||
{/* طبق خواستهی کاربر: نام+لوگوی فروشگاه راست (فرزند اول)، عینِ اندروید، و
|
||||
اشتراکگذاری روبروی نام سمت چپ (فرزند آخر) */}
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
|
||||
{shopInfo?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shopInfo.logo}
|
||||
alt={shopInfo.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={18}
|
||||
height={18}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void shareProduct()}
|
||||
aria-label={t("shops.share.title")}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="share" size={18} tinted />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* گردی گوشههای کادر ۱۶px (rounded-2xl) عینِ اندروید (ProductDetailScreen.tsx:
|
||||
card.borderRadius=16) — قبلاً rounded-3xl (۲۴px) بود */}
|
||||
<div className="mx-auto mt-4 flex w-full max-w-md flex-col gap-4 rounded-2xl border border-neutral-200 p-4 dark:border-neutral-700">
|
||||
{/* عینِ اندروید: عنوان راست (فرزند اول)، بجِ تخفیف چپ (فرزند آخر) — قبلاً
|
||||
برعکس بود (بج قبل از عنوان) */}
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
{discountPercent != null && <DiscountBadge percent={discountPercent} />}
|
||||
<h1 className="text-xl font-bold">{listing.title}</h1>
|
||||
{discountPercent != null && <DiscountBadge percent={discountPercent} />}
|
||||
</div>
|
||||
|
||||
{images.length > 0 && (
|
||||
|
||||
@@ -138,7 +138,11 @@ function ProductNamePage() {
|
||||
<LocalePageShell>
|
||||
<AuthPageLayout>
|
||||
<div className="flex w-full max-w-sm justify-start">
|
||||
<BackButton />
|
||||
{/* پیشفرضِ router.back() اینجا کاربر رو برمیگردوند به هرجایی که ازش وارد این
|
||||
ویزارد شده — که برای ویرایش، صفحهی نمایشِ کالا
|
||||
(shops/[shopId]/products/[listingId]) بود، نه لیستِ کالاها. طبقِ خواستهی
|
||||
کاربر همیشه صریحاً میره به لیستِ کالاهای همین فروشگاه */}
|
||||
<BackButton href={`/shops/${shopId}`} />
|
||||
</div>
|
||||
<AuthPageContent>
|
||||
<span className="text-xl font-bold mb-4">{t("shops.productNameTitle")}</span>
|
||||
|
||||
@@ -297,6 +297,7 @@ function ProductVariantsPage() {
|
||||
loading={loading}
|
||||
disabled={loading || rows.length === 0}
|
||||
onClick={handleSubmit}
|
||||
className="border-[#ef4444] bg-[#ef4444] text-white hover:bg-[#ef4444]/90 dark:bg-[#ef4444] dark:hover:bg-[#ef4444]/90"
|
||||
>
|
||||
{t("shops.saveProduct")}
|
||||
</AuthNextButton>
|
||||
|
||||
@@ -6,7 +6,7 @@ import PageTitle from "@/components/settings/PageTitle";
|
||||
import AuthNextButton from "@/components/auth/AuthNextButton";
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import { useParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Image from "next/image";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -34,7 +34,6 @@ type OrderDetail = {
|
||||
|
||||
export default function CheckoutPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const params = useParams<{ orderId: string }>();
|
||||
const { request, loading } = useAxios();
|
||||
const [order, setOrder] = useState<OrderDetail | null>(null);
|
||||
@@ -54,7 +53,10 @@ export default function CheckoutPage() {
|
||||
{ orderId: params.orderId }
|
||||
);
|
||||
if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
// router.push (next/navigation) برای URLهای بیرونی رفتارِ تضمینشدهای نداره —
|
||||
// window.location.href مطمئنترین راهه (خصوصاً سافاری) و با بقیهی مسیرهای
|
||||
// پرداختِ این اپ (booking، آکادمی) هم یکی میمونه
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
const message =
|
||||
|
||||
@@ -10,7 +10,9 @@ import BackButton from "@/components/ui/BackButton";
|
||||
import DiscountBadge from "@/components/ui/DiscountBadge";
|
||||
import TabNavigation from "@/components/TabNavigation";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { copyTextToClipboard } from "@/lib/chat/getCopyableMessageText";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { btnDefault, btnPrimary } from "@/lib/ui/buttonStyles";
|
||||
import { IShopProductListing, IShopProductVariant } from "@/types/types";
|
||||
import Cookies from "js-cookie";
|
||||
import { trackExploreInteraction } from "@/api/trackExploreInteraction";
|
||||
@@ -94,6 +96,24 @@ export default function ListingDetailClient() {
|
||||
const shopInfo: ShopHeader | null =
|
||||
listing && typeof listing.shop === "object" ? (listing.shop as ShopHeader) : null;
|
||||
|
||||
// طبق خواستهی کاربر: اشتراکگذاری لینکِ خودِ کالا، نه فروشگاه — عینِ shareProduct
|
||||
// توی ProductDetailScreen.tsx اندروید (Web Share API با fallback به کپیِ لینک)
|
||||
const shareProduct = async () => {
|
||||
if (!listing?._id) return;
|
||||
const productUrl = `https://modstagram.com/shops/listing/${listing._id}`;
|
||||
if (typeof navigator !== "undefined" && navigator.share) {
|
||||
try {
|
||||
await navigator.share({ url: productUrl, title: listing.title });
|
||||
return;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const ok = await copyTextToClipboard(productUrl);
|
||||
if (ok) toast.success(t("shops.share.copied"));
|
||||
else toast.error(t("shops.share.copyFailed"));
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (!listing?._id) return;
|
||||
const token = Cookies.get("token") || "";
|
||||
@@ -164,6 +184,10 @@ export default function ListingDetailClient() {
|
||||
}, [matchedVariant?._id]);
|
||||
|
||||
const enabledShippingMethods = shopInfo?.shipping_methods?.filter((m) => m.enabled) || [];
|
||||
// هزینهی روشِ ارسالِ انتخابشده — قبلاً هیچجا خونده نمیشد، پس انتخابِ روشِ ارسال
|
||||
// هیچ تاثیری روی قیمتِ نمایشدادهشده نداشت (فقط موقعِ ثبتِ سفارش/handleBuy چک میشد)
|
||||
const shippingCost =
|
||||
enabledShippingMethods.find((m) => m.method === shippingMethod)?.cost || 0;
|
||||
|
||||
const categoryLine = listing
|
||||
? [listing.category, listing.sub_category, listing.sub_sub_category]
|
||||
@@ -256,34 +280,47 @@ export default function ListingDetailClient() {
|
||||
<div className="flex w-full">
|
||||
<BackButton />
|
||||
</div>
|
||||
{/* طبق خواستهی کاربر: دکمهی اشتراکگذاریِ خودِ کالا (نه فروشگاه) روبروی نام
|
||||
فروشگاه سمت چپ — عینِ ProductDetailScreen.tsx اندروید */}
|
||||
{shopInfo?._id ? (
|
||||
<Link
|
||||
href={`/shops/profile/${shopInfo._id}`}
|
||||
className="flex items-center justify-end gap-2"
|
||||
>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
|
||||
{shopInfo?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shopInfo.logo}
|
||||
alt={shopInfo.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={18}
|
||||
height={18}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
</Link>
|
||||
<div className="flex w-full items-center justify-between gap-2">
|
||||
<Link
|
||||
href={`/shops/profile/${shopInfo._id}`}
|
||||
className="flex items-center gap-2"
|
||||
>
|
||||
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
|
||||
{shopInfo?.logo ? (
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
src={IMAGE_BASE_URL + shopInfo.logo}
|
||||
alt={shopInfo.name}
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
) : (
|
||||
<Image
|
||||
src={staticIconUrl("/images/icons/vuesax/bold/shop.svg")}
|
||||
width={18}
|
||||
height={18}
|
||||
alt=""
|
||||
className="dark:invert"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
<span className="text-sm font-bold">{shopInfo?.name}</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void shareProduct()}
|
||||
aria-label={t("shops.share.title")}
|
||||
className="flex h-9 w-9 shrink-0 items-center justify-center"
|
||||
>
|
||||
<BoldIcon name="share" size={18} tinted />
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="mx-auto mt-4 flex w-full max-w-md flex-col gap-4 rounded-3xl border border-neutral-200 p-4 dark:border-neutral-700">
|
||||
{/* گردی گوشههای کادر ۱۶px (rounded-2xl) عینِ اندروید — قبلاً rounded-3xl (۲۴px) بود */}
|
||||
<div className="mx-auto mt-4 flex w-full max-w-md flex-col gap-4 rounded-2xl border border-neutral-200 p-4 dark:border-neutral-700">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h1 className="text-right text-xl font-bold">{listing.title}</h1>
|
||||
{discountPercent != null && (
|
||||
@@ -432,14 +469,14 @@ export default function ListingDetailClient() {
|
||||
</SpecRow>
|
||||
)}
|
||||
|
||||
{/* عین ProductDetailScreen.tsx اندروید: دکمهی ثبت خرید با colors.success (#387E65)،
|
||||
{/* عین ProductDetailScreen.tsx اندروید: دکمهی ثبت خرید با colors.danger (#ef4444)،
|
||||
و اسپینر تعداد یک پیل بوردردار خنثی با دکمههای +/- متنی، نه دو دکمهی دایرهای رنگی */}
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleBuy()}
|
||||
disabled={loading || !matchedVariant}
|
||||
className="flex-1 rounded-2xl bg-[#387E65] py-2.5 text-center text-sm font-bold text-white disabled:opacity-50"
|
||||
className="flex-1 rounded-2xl bg-[#ef4444] py-2.5 text-center text-sm font-bold text-white disabled:opacity-50"
|
||||
>
|
||||
{t("shops.placeOrderButton")}
|
||||
</button>
|
||||
@@ -471,15 +508,15 @@ export default function ListingDetailClient() {
|
||||
{matchedVariant.discount_price ? (
|
||||
<span className="flex items-center gap-2">
|
||||
<span className="text-xs text-neutral-400 line-through">
|
||||
{(matchedVariant.price * quantity).toLocaleString()} {t("settings.toman")}
|
||||
{(matchedVariant.price * quantity + shippingCost).toLocaleString()} {t("settings.toman")}
|
||||
</span>
|
||||
<span className="text-sm font-bold text-red-500">
|
||||
{(matchedVariant.discount_price * quantity).toLocaleString()} {t("settings.toman")}
|
||||
{(matchedVariant.discount_price * quantity + shippingCost).toLocaleString()} {t("settings.toman")}
|
||||
</span>
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-sm font-bold">
|
||||
{(matchedVariant.price * quantity).toLocaleString()} {t("settings.toman")}
|
||||
{(matchedVariant.price * quantity + shippingCost).toLocaleString()} {t("settings.toman")}
|
||||
</span>
|
||||
)}
|
||||
</SpecRow>
|
||||
@@ -487,19 +524,19 @@ export default function ListingDetailClient() {
|
||||
</div>
|
||||
|
||||
<div className="mx-auto mb-6 mt-8 flex w-full max-w-md gap-3">
|
||||
{/* عین ProductDetailScreen.tsx اندروید: سبد خرید با colors.info (#17337B) توپر،
|
||||
علاقهمندی فقط حاشیهدارِ همون رنگ (نه پرشده) */}
|
||||
{/* طبق خواستهی کاربر: همون رنگ/استایلِ دکمههای اعمال/پاککردنِ فیلترها —
|
||||
سبد خرید مثل «اعمال» (btnPrimary)، علاقهمندی مثل «پاک کردن» (btnDefault) */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAddToCart}
|
||||
className="flex-1 rounded-2xl bg-[#17337B] py-3 text-sm font-bold text-white"
|
||||
className={cn(btnPrimary, "flex-1 py-3 text-sm")}
|
||||
>
|
||||
{t("shops.addToCartButton")}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleToggleFavorite}
|
||||
className="flex-1 rounded-2xl border border-[#17337B] py-3 text-sm font-bold text-[#17337B]"
|
||||
className={cn(btnDefault, "flex-1 py-3 text-sm")}
|
||||
>
|
||||
{favorite ? t("shops.removeFromFavoritesButton") : t("shops.addToFavoritesButton")}
|
||||
</button>
|
||||
|
||||
@@ -129,8 +129,62 @@ function ShopLocationPage() {
|
||||
<AuthPageContent>
|
||||
<span className="text-xl font-bold mb-4">{t("shops.locationTitle")}</span>
|
||||
|
||||
{/* طبق خواستهی کاربر: استان/شهر همیشه باز و اجباریان (چه حضوری داشته باشه چه
|
||||
نه)، فقط نقشه/محله/آدرسِ دقیق مخصوصِ حالتِ حضوریه و مخفی میشن؛ تیکِ «فروشگاه
|
||||
حضوری ندارم» هم رفت زیرِ آدرس — عینِ اندروید (NewShopLocationScreen.tsx) */}
|
||||
<div className="flex w-full max-w-sm flex-col gap-2">
|
||||
<label className="mb-2 flex items-center gap-2">
|
||||
<CountryProvinceCitySelect
|
||||
value={geoSelection}
|
||||
onChange={setGeoSelection}
|
||||
lockToUserCountry
|
||||
/>
|
||||
|
||||
{hasPhysicalLocation && (
|
||||
<>
|
||||
<div className="mt-2 w-full overflow-hidden rounded-2xl">
|
||||
<Map
|
||||
style={{ height: "220px" }}
|
||||
initialViewState={{
|
||||
longitude: marker?.lng || 51.375433528216654,
|
||||
latitude: marker?.lat || 35.73356434056531,
|
||||
zoom: 11,
|
||||
}}
|
||||
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
|
||||
mapStyle="mapbox://styles/mapbox/streets-v11"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<GeolocateControl />
|
||||
{marker && (
|
||||
<Marker latitude={marker.lat} longitude={marker.lng}>
|
||||
<Image
|
||||
alt="location icon"
|
||||
className="-mt-5"
|
||||
width={25}
|
||||
height={25}
|
||||
src="/images/icons/location.svg"
|
||||
/>
|
||||
</Marker>
|
||||
)}
|
||||
</Map>
|
||||
</div>
|
||||
|
||||
<RoundedInput
|
||||
type="text"
|
||||
className="mt-2 text-xs"
|
||||
placeholder={t("shops.neighbourhoodPlaceholder")}
|
||||
value={neighbourhood}
|
||||
onChange={(e) => setNeighbourhood(e.target.value)}
|
||||
/>
|
||||
<textarea
|
||||
className="mt-2 h-24 w-full rounded-3xl border border-neutral-950 bg-white p-4 font-medium dark:border-neutral-400 dark:bg-neutral-950 dark:text-neutral-50"
|
||||
placeholder={t("shops.addressPlaceholder")}
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<label className="mt-2 flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="scale-125"
|
||||
@@ -139,53 +193,6 @@ function ShopLocationPage() {
|
||||
/>
|
||||
<span className="text-sm">{t("shops.noPhysicalShop")}</span>
|
||||
</label>
|
||||
|
||||
<CountryProvinceCitySelect
|
||||
value={geoSelection}
|
||||
onChange={setGeoSelection}
|
||||
lockToUserCountry
|
||||
/>
|
||||
|
||||
<div className="mt-2 w-full overflow-hidden rounded-2xl">
|
||||
<Map
|
||||
style={{ height: "220px" }}
|
||||
initialViewState={{
|
||||
longitude: marker?.lng || 51.375433528216654,
|
||||
latitude: marker?.lat || 35.73356434056531,
|
||||
zoom: 11,
|
||||
}}
|
||||
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
|
||||
mapStyle="mapbox://styles/mapbox/streets-v11"
|
||||
onClick={handleMapClick}
|
||||
>
|
||||
<GeolocateControl />
|
||||
{marker && (
|
||||
<Marker latitude={marker.lat} longitude={marker.lng}>
|
||||
<Image
|
||||
alt="location icon"
|
||||
className="-mt-5"
|
||||
width={25}
|
||||
height={25}
|
||||
src="/images/icons/location.svg"
|
||||
/>
|
||||
</Marker>
|
||||
)}
|
||||
</Map>
|
||||
</div>
|
||||
|
||||
<RoundedInput
|
||||
type="text"
|
||||
className="mt-2 text-xs"
|
||||
placeholder={t("shops.neighbourhoodPlaceholder")}
|
||||
value={neighbourhood}
|
||||
onChange={(e) => setNeighbourhood(e.target.value)}
|
||||
/>
|
||||
<textarea
|
||||
className="mt-2 h-24 w-full rounded-3xl border border-neutral-950 bg-white p-4 font-medium dark:border-neutral-400 dark:bg-neutral-950 dark:text-neutral-50"
|
||||
placeholder={t("shops.addressPlaceholder")}
|
||||
value={address}
|
||||
onChange={(e) => setAddress(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</AuthPageContent>
|
||||
<AuthFormFooter>
|
||||
|
||||
@@ -46,8 +46,17 @@ function ShopLogoPage() {
|
||||
const goNext = () => router.push("/shops/new/shipping");
|
||||
|
||||
const handleUpload = async () => {
|
||||
if (!logoFile || !shopId) {
|
||||
toast.error(t("shops.logoRequired"));
|
||||
if (!shopId) return;
|
||||
// اگه کاربر فایلِ جدید انتخاب نکرده ولی از قبل لوگو داره (ویرایشِ فروشگاهِ
|
||||
// موجود، logoPreview از GET /shops/:id پر شده)، نباید پیامِ «لطفاً لوگو رو
|
||||
// انتخاب کنید» نشون داده بشه — قبلاً اینجا فقط logoFile چک میشد نه
|
||||
// logoPreview، پس با وجودِ لوگوی از قبلثبتشده هم گیر میکرد
|
||||
if (!logoFile) {
|
||||
if (logoPreview) {
|
||||
goNext();
|
||||
} else {
|
||||
toast.error(t("shops.logoRequired"));
|
||||
}
|
||||
return;
|
||||
}
|
||||
const formData = new FormData();
|
||||
|
||||
@@ -43,7 +43,9 @@ export default function ShopPaymentFailedPage() {
|
||||
{ orderId }
|
||||
);
|
||||
if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
}
|
||||
} catch {
|
||||
// toast already shown by the axios interceptor
|
||||
|
||||
@@ -90,7 +90,7 @@ export default function ProductComparisonPage() {
|
||||
{discountPercent != null && (
|
||||
<DiscountBadge
|
||||
percent={discountPercent}
|
||||
className="absolute right-1 top-1"
|
||||
className="absolute left-1 top-1"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -11,6 +11,7 @@ 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 VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
|
||||
import { useUser } from "@/hooks/useUser";
|
||||
@@ -52,6 +53,7 @@ type PublicShop = {
|
||||
owner?: {
|
||||
_id: string;
|
||||
user_name: string;
|
||||
verify_badge?: string | null;
|
||||
} | null;
|
||||
productCount: number;
|
||||
rating: { average: number; count: number };
|
||||
@@ -205,45 +207,49 @@ export default function ShopProfileClient() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid w-full grid-cols-3 items-end py-2 text-xs font-semibold md:text-sm">
|
||||
<div className="flex items-center">
|
||||
<span>{shop.rating.count > 0 ? shop.rating.average.toFixed(1) : "0"}</span>
|
||||
<Image width={22} height={22} alt="star icon" src="/images/icons/star1.png" />
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<span>{shop.rating.count}</span>
|
||||
<Image width={22} height={22} alt="rate icon" src="/images/icons/medal-star.png" />
|
||||
{/* طبق خواستهی کاربر: توضیحاتِ فروشگاه درست زیرِ نام، و برچسبِ نوعِ فروش
|
||||
(«حضوری»/«آنلاین») درست زیرِ ریتوامتیاز — یعنی دو ستونِ عمودی کنارِ هم
|
||||
بهجای یک ردیفِ مشترک؛ عینِ اندروید (ShopProfileScreen.tsx: identityRow) */}
|
||||
<div className="flex w-full items-start justify-between gap-2 py-2 text-xs font-semibold md:text-sm">
|
||||
<div className="flex min-w-0 shrink-0 flex-col gap-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<span>{shop.rating.count > 0 ? shop.rating.average.toFixed(1) : "0"}</span>
|
||||
<Image width={22} height={22} alt="star icon" src="/images/icons/star1.png" />
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span>{shop.rating.count}</span>
|
||||
<Image width={22} height={22} alt="rate icon" src="/images/icons/medal-star.png" />
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 leading-6 text-[#387E65]",
|
||||
isFa ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
{shop.has_physical_location ? t("shops.inPerson") : t("shops.onlineOnly")}
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 max-w-full flex-col",
|
||||
"flex min-w-0 max-w-full flex-col gap-1",
|
||||
isFa ? "items-end" : "items-start"
|
||||
)}
|
||||
>
|
||||
<h3
|
||||
className={cn(
|
||||
"max-w-full truncate whitespace-nowrap",
|
||||
"flex max-w-full items-center gap-1 truncate whitespace-nowrap",
|
||||
isFa ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
{shop.name}
|
||||
<VerificationBadge verifyBadge={shop.owner?.verify_badge} size={16} />
|
||||
</h3>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-full py-2 text-xs font-semibold md:text-sm">
|
||||
<div
|
||||
className={cn(
|
||||
"flex w-full gap-3",
|
||||
hasDescription
|
||||
? "items-baseline justify-between"
|
||||
: "items-center justify-end"
|
||||
)}
|
||||
>
|
||||
{hasDescription ? (
|
||||
<div
|
||||
className={cn(
|
||||
"min-w-0 flex-1 leading-6",
|
||||
"min-w-0 max-w-full leading-6",
|
||||
isFa ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
@@ -253,14 +259,6 @@ export default function ShopProfileClient() {
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div
|
||||
className={cn(
|
||||
"shrink-0 leading-6 text-[#387E65]",
|
||||
isFa ? "text-right" : "text-left"
|
||||
)}
|
||||
>
|
||||
{shop.has_physical_location ? t("shops.inPerson") : t("shops.onlineOnly")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -318,7 +316,15 @@ export default function ShopProfileClient() {
|
||||
{t("shops.noProductsYet")}
|
||||
</p>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 gap-0.5 sm:gap-1">
|
||||
// چیدمانِ این صفحه ambient dir معکوسیه (dir={isFa?"ltr":"rtl"} چند خط
|
||||
// بالاتر، برای بخشهای دیگهی همین صفحه). گریدِ عکسِ کالاها باید عینِ
|
||||
// اندروید (چیدمانِ عادی/غیرمعکوس، راستبهچپ برای فارسی) باشه، پس اینجا
|
||||
// با dir عادی override میشه. گردی هم قبلاً responsive (۶px زیرِ sm، ۱۶px
|
||||
// بالاش) بود، الان همیشه ۱۶px — عینِ tileImg توی ShopProfileScreen.tsx
|
||||
<div
|
||||
className="grid grid-cols-3 gap-0.5 sm:gap-1"
|
||||
dir={isFa ? "rtl" : "ltr"}
|
||||
>
|
||||
{listings.map((listing) => {
|
||||
const primaryImage =
|
||||
listing.images?.[listing.primaryImageIndex] || listing.images?.[0];
|
||||
@@ -330,7 +336,7 @@ export default function ShopProfileClient() {
|
||||
onClick={() =>
|
||||
router.push(`/shops/profile/${shopId}/reel/${tileId}`)
|
||||
}
|
||||
className="relative aspect-square overflow-hidden rounded-md bg-neutral-100 sm:rounded-2xl dark:bg-neutral-800"
|
||||
className="relative aspect-square overflow-hidden rounded-2xl bg-neutral-100 dark:bg-neutral-800"
|
||||
>
|
||||
{listing.isTaggedPost && (
|
||||
<span className="absolute right-1 top-1 z-10 rounded-full bg-black/50 px-1.5 py-0.5 text-[9px] font-semibold text-white">
|
||||
|
||||
@@ -51,16 +51,25 @@ export default function TabNavigation({ currentPage }: TabNavigationProps) {
|
||||
(currentPage.startsWith("/projects") ||
|
||||
currentPage === "/new-project")) ||
|
||||
(tab.href === "/academy" && currentPage.startsWith("/academy"));
|
||||
// isActive فقط برای رنگ/هایلایتِ آیکونه — شاملِ زیرصفحهها هم میشه (مثلاً
|
||||
// /settings/wallet). اگه کلیک روی خودِ همین isActive شرط بشه، وقتی از یه
|
||||
// زیرصفحهی تنظیمات روی آیکونِ تنظیمات بزنی، چون isActive از قبل true هست
|
||||
// preventDefault صدا زده میشه و Link اصلاً navigate نمیکنه — کاربر مجبور
|
||||
// میشد اول یه تبِ دیگه بزنه بعد دوباره تنظیمات، تا واقعاً برگرده به /settings.
|
||||
// اینترسپت (برای اسکرولبهبالا/رویدادِ تب) فقط وقتی درسته که دقیقاً همون
|
||||
// مسیرِ رَوتِ تب باشیم؛ وگرنه باید بذاریم Link عادی navigate کنه
|
||||
const isExactRoot = currentPage === tab.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={tab.href}
|
||||
href={tab.href}
|
||||
onClick={(e) => {
|
||||
// وقتی تبی که از قبل توش هستیم دوباره زده میشه، Next.js چون مسیر عوض
|
||||
// نمیشه هیچ اسکرول/رفرشی انجام نمیده — طبق درخواست کاربر، مثل اینستاگرام
|
||||
// باید صفحه بره بالا و (برای تبِ خانه) جدیدترین پستها هم لود بشن
|
||||
if (isActive) {
|
||||
// وقتی تبی که از قبل توش هستیم (دقیقاً همون صفحهی روتش) دوباره زده
|
||||
// میشه، Next.js چون مسیر عوض نمیشه هیچ اسکرول/رفرشی انجام نمیده —
|
||||
// طبق درخواست کاربر، مثل اینستاگرام باید صفحه بره بالا و (برای تبِ
|
||||
// خانه) جدیدترین پستها هم لود بشن
|
||||
if (isExactRoot) {
|
||||
e.preventDefault();
|
||||
window.dispatchEvent(
|
||||
new CustomEvent("app:tab-reselect", {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import Modal from "@/components/elements/Modal";
|
||||
import RoundedButton from "@/components/elements/RoundedButton";
|
||||
import RoundedInput from "@/components/elements/RoundedInput";
|
||||
@@ -38,7 +37,6 @@ export default function GiftInChatModal({
|
||||
}: GiftInChatModalProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const { request } = useAxios();
|
||||
const router = useRouter();
|
||||
|
||||
const [selectedAmount, setSelectedAmount] = useState<number | null>(PRESET_AMOUNTS[0]);
|
||||
const [customAmount, setCustomAmount] = useState("");
|
||||
@@ -79,7 +77,9 @@ export default function GiftInChatModal({
|
||||
reset();
|
||||
onClose();
|
||||
} else if (response?.paymentUrl) {
|
||||
router.push(response.paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = response.paymentUrl;
|
||||
} else {
|
||||
toast.error(t("giftPage.paymentInfoError"));
|
||||
}
|
||||
|
||||
@@ -326,12 +326,9 @@ export function CourseManager() {
|
||||
}
|
||||
|
||||
if (paymentUrl) {
|
||||
window.open(paymentUrl, "_blank", "noopener,noreferrer");
|
||||
toast.success(t("academyCourse.redirectedToPayment"));
|
||||
|
||||
setTimeout(() => {
|
||||
setIsDialogOpen1?.(false);
|
||||
}, 1000);
|
||||
// window.open بعدِ await در سافاری (دسکتاپ/iOS) پاپآپبلاک میشه چون دیگه
|
||||
// synchronously به کلیکِ کاربر متصل نیست — window.location.href مطمئنتره
|
||||
window.location.href = paymentUrl;
|
||||
} else {
|
||||
console.error("ساختار پاسخ نامعتبر:", response);
|
||||
toast.error(
|
||||
|
||||
@@ -199,16 +199,14 @@ function ModelsFilter({ expertise }: ModelsFilterProps) {
|
||||
>
|
||||
<BoldIcon name="grid-3" size={TOOLBAR_ICON_SIZE} className="block" />
|
||||
</button>
|
||||
{/* طبق خواستهی صریح کاربر: فعلاً که تعداد فروشگاهها کمه این آیکون مخفیه؛
|
||||
هر وقت تعداد فروشگاهها زیاد شد از کامنت درش بیارید */}
|
||||
{/* <button
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => router.push("/shops/discover")}
|
||||
aria-label={t("filters.toolbar.shop")}
|
||||
className={toolbarIconButtonClass}
|
||||
>
|
||||
<BoldIcon name="shop" size={TOOLBAR_ICON_SIZE} className="block" />
|
||||
</button> */}
|
||||
</button>
|
||||
</div>
|
||||
{showFilterModal && (
|
||||
<FilterModal
|
||||
|
||||
@@ -8,7 +8,6 @@ import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { IProjectRequest } from "@/types/types";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
@@ -24,7 +23,6 @@ function ProjectRequestsAction({
|
||||
}) {
|
||||
const { t } = useTranslation("common");
|
||||
const { request } = useAxios();
|
||||
const router = useRouter();
|
||||
const [showConfirmModal, setShowConfirmModal] = useState(false);
|
||||
const [itemToAccept, setItemToAccept] = useState<IProjectRequest>();
|
||||
const [viewerUrl, setViewerUrl] = useState<string | null>(null);
|
||||
@@ -47,10 +45,12 @@ function ProjectRequestsAction({
|
||||
projectId,
|
||||
}
|
||||
);
|
||||
// لینک خارجی — router.push (next/navigation) رفتارِ تضمینشدهای برای URLهای
|
||||
// بیرونی نداره؛ window.location.href مطمئنتره (خصوصاً سافاری)
|
||||
if (response?.paymentUrl) {
|
||||
router.push(response.paymentUrl);
|
||||
window.location.href = response.paymentUrl;
|
||||
} else if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -39,7 +39,9 @@ function ProjectWorkroomActions({
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -73,10 +73,13 @@ function ProjectWorkroomRate({
|
||||
"/projects/initiate-completion-payment/web",
|
||||
{ projectId }
|
||||
);
|
||||
// لینک خارجی — router.push (next/navigation) رفتارِ تضمینشدهای برای URLهای
|
||||
// بیرونی نداره؛ window.location.href مطمئنتره (خصوصاً سافاری). sessionStorage
|
||||
// بالا هنوز کار میکنه چون همین تب navigate میشه، نه یه تبِ جدید
|
||||
if (response?.paymentUrl) {
|
||||
router.push(response.paymentUrl);
|
||||
window.location.href = response.paymentUrl;
|
||||
} else if (response?.authority) {
|
||||
router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`);
|
||||
window.location.href = `https://www.zarinpal.com/pg/StartPay/${response.authority}`;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
const message =
|
||||
|
||||
@@ -42,7 +42,9 @@ function MyBillboardStatusActions({
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
@@ -65,7 +67,9 @@ function MyBillboardStatusActions({
|
||||
} else {
|
||||
const authority = response.authority;
|
||||
const paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`;
|
||||
router.push(paymentUrl);
|
||||
// لینک خارجی — router.push رفتارِ تضمینشدهای نداره؛ window.location.href
|
||||
// مطمئنتره (خصوصاً سافاری)
|
||||
window.location.href = paymentUrl;
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
console.log(error);
|
||||
|
||||
@@ -54,7 +54,9 @@ export default function ProductCategoryPicker({
|
||||
type="button"
|
||||
className={cn(
|
||||
toggleBtnClass(category === item.category),
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight"
|
||||
// گردی toggleBtnClass پیشفرض rounded-3xl(24px) هست؛ اینجا عینِ pill
|
||||
// توی NewProductCategoryScreen.tsx اندروید (borderRadius:16) override میشه
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight !rounded-2xl"
|
||||
)}
|
||||
onClick={() => onCategorySelect(item.category)}
|
||||
>
|
||||
@@ -76,7 +78,9 @@ export default function ProductCategoryPicker({
|
||||
type="button"
|
||||
className={cn(
|
||||
toggleBtnClass(subCategory === item.name),
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight"
|
||||
// گردی toggleBtnClass پیشفرض rounded-3xl(24px) هست؛ اینجا عینِ pill
|
||||
// توی NewProductCategoryScreen.tsx اندروید (borderRadius:16) override میشه
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight !rounded-2xl"
|
||||
)}
|
||||
onClick={() => onSubCategorySelect(item.name)}
|
||||
>
|
||||
@@ -100,7 +104,9 @@ export default function ProductCategoryPicker({
|
||||
type="button"
|
||||
className={cn(
|
||||
toggleBtnClass(subSubCategory === item.name),
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight"
|
||||
// گردی toggleBtnClass پیشفرض rounded-3xl(24px) هست؛ اینجا عینِ pill
|
||||
// توی NewProductCategoryScreen.tsx اندروید (borderRadius:16) override میشه
|
||||
"min-h-9 p-1.5 text-[11px] leading-tight !rounded-2xl"
|
||||
)}
|
||||
onClick={() => onSubSubCategorySelect(item.name)}
|
||||
>
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function ShopCartTab() {
|
||||
{discountPercent != null && (
|
||||
<DiscountBadge
|
||||
percent={discountPercent}
|
||||
className="absolute right-1 top-1"
|
||||
className="absolute left-1 top-1"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
|
||||
@@ -26,22 +26,32 @@ const ShopContactInfoModal: React.FC<IShopContactInfoModalProps> = ({
|
||||
contactInfo,
|
||||
}) => {
|
||||
const { t } = useTranslation("common");
|
||||
// عین ShopProfileScreen.tsx اندروید: آیکونِ برندِ رنگی (نه آیکونِ عمومیِ تکرنگِ
|
||||
// قبلی که با dark:invert معکوس میشد)
|
||||
const contactMethods = [
|
||||
{ key: "mobile", icon: "mobile.svg", link: `tel:${contactInfo?.mobile}` },
|
||||
{ key: "landline", icon: "phone.svg", link: `tel:${contactInfo?.landline}` },
|
||||
{
|
||||
key: "mobile",
|
||||
icon: "brand/phone-brand.svg",
|
||||
link: `tel:${contactInfo?.mobile}`,
|
||||
},
|
||||
{
|
||||
key: "landline",
|
||||
icon: "brand/phone-brand.svg",
|
||||
link: `tel:${contactInfo?.landline}`,
|
||||
},
|
||||
{
|
||||
key: "whatsapp",
|
||||
icon: "whatsapp.svg",
|
||||
icon: "brand/whatsapp-brand.svg",
|
||||
link: `https://wa.me/${contactInfo?.whatsapp}`,
|
||||
},
|
||||
{
|
||||
key: "telegram",
|
||||
icon: "telegram.svg",
|
||||
icon: "brand/telegram-brand.svg",
|
||||
link: `https://t.me/${contactInfo?.telegram}`,
|
||||
},
|
||||
{
|
||||
key: "instagram",
|
||||
icon: "instagram.svg",
|
||||
icon: "brand/instagram-brand.svg",
|
||||
link: `https://instagram.com/${contactInfo?.instagram}`,
|
||||
},
|
||||
];
|
||||
@@ -64,11 +74,11 @@ const ShopContactInfoModal: React.FC<IShopContactInfoModalProps> = ({
|
||||
<RoundedDiv className="h-9 w-full gap-2">
|
||||
<span>{contactInfo[key as keyof IShopContactInfo]}</span>
|
||||
<Image
|
||||
width={24}
|
||||
height={24}
|
||||
width={22}
|
||||
height={22}
|
||||
alt={icon}
|
||||
src={`/images/icons/${icon}`}
|
||||
className="dark:invert"
|
||||
unoptimized
|
||||
/>
|
||||
</RoundedDiv>
|
||||
</a>
|
||||
|
||||
@@ -85,7 +85,7 @@ export default function ShopFavoritesTab() {
|
||||
{discountPercent != null && (
|
||||
<DiscountBadge
|
||||
percent={discountPercent}
|
||||
className="absolute right-2 top-2"
|
||||
className="absolute left-2 top-2"
|
||||
/>
|
||||
)}
|
||||
<button
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
|
||||
type ShopProductAuthorProps = {
|
||||
shopName: string;
|
||||
shopLogo?: string | null;
|
||||
shopVerifyBadge?: string | null;
|
||||
};
|
||||
|
||||
export default function ShopProductAuthor({
|
||||
shopName,
|
||||
shopLogo,
|
||||
shopVerifyBadge,
|
||||
}: ShopProductAuthorProps) {
|
||||
return (
|
||||
<span className="flex min-w-0 items-center gap-1.5">
|
||||
@@ -23,8 +26,9 @@ export default function ShopProductAuthor({
|
||||
) : (
|
||||
<span className="h-8 w-8 shrink-0 rounded-full bg-neutral-300 dark:bg-neutral-700" />
|
||||
)}
|
||||
<span className="truncate text-xs font-medium text-neutral-600 dark:text-neutral-300">
|
||||
<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} />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
@@ -16,6 +16,7 @@ type ShopProductGridProps = {
|
||||
/** Fallback shop name/logo used when `listing.shop` isn't a populated object (single-shop pages). */
|
||||
shopName?: string;
|
||||
shopLogo?: string | null;
|
||||
shopVerifyBadge?: string | null;
|
||||
isLoading?: boolean;
|
||||
/** Defaults to the Torob-style price-comparison page for the listing's catalog product. */
|
||||
getHref?: (listing: IShopProductListing) => string;
|
||||
@@ -71,6 +72,7 @@ export default function ShopProductGrid({
|
||||
listings,
|
||||
shopName,
|
||||
shopLogo,
|
||||
shopVerifyBadge,
|
||||
isLoading,
|
||||
getHref = defaultHref,
|
||||
}: ShopProductGridProps) {
|
||||
@@ -147,7 +149,7 @@ export default function ShopProductGrid({
|
||||
{discountPercent != null && (
|
||||
<DiscountBadge
|
||||
percent={discountPercent}
|
||||
className="absolute right-2 top-2"
|
||||
className="absolute left-2 top-2"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@@ -156,6 +158,7 @@ export default function ShopProductGrid({
|
||||
<ShopProductAuthor
|
||||
shopName={cardShop?.name || shopName || ""}
|
||||
shopLogo={cardShop?.logo ?? shopLogo}
|
||||
shopVerifyBadge={cardShop?.owner?.verify_badge ?? shopVerifyBadge}
|
||||
/>
|
||||
<span className="truncate text-xs font-semibold">
|
||||
{listing.title}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import DiscountBadge from "@/components/ui/DiscountBadge";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { bestDiscountPercent } from "@/lib/shopDiscount";
|
||||
@@ -47,10 +48,18 @@ export default function ShopProductListRow({ listing, shopId }: ShopProductListR
|
||||
const inStock = isInStock(listing);
|
||||
const category = leafCategory(listing);
|
||||
|
||||
// طبق خواستهی کاربر: کالاهایی که هنوز تاییدِ ادمین نگرفتن (pending_shop_approval)
|
||||
// مخفی نشن، فقط کل کارت کمرنگ بشه تا مشخص باشه، و یه تیکِ «در دست بررسی» کنارِ
|
||||
// عکسِ کالا نمایش داده بشه — عینِ اندروید (ShopProductListRow.tsx)
|
||||
const isPendingApproval = listing.status === "pending_shop_approval";
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/shops/${shopId}/products/${listing._id}`}
|
||||
className="gentle-transition flex items-center gap-3 rounded-[28px] border border-neutral-200 p-3 active:scale-[0.99] dark:border-neutral-700"
|
||||
className={cn(
|
||||
"gentle-transition flex items-center gap-3 rounded-[28px] border border-neutral-200 p-3 active:scale-[0.99] dark:border-neutral-700",
|
||||
listing.status !== "active" && "opacity-50"
|
||||
)}
|
||||
>
|
||||
<div className="relative h-20 w-20 shrink-0 overflow-hidden rounded-2xl bg-neutral-100 dark:bg-neutral-800">
|
||||
{primaryImage && (
|
||||
@@ -64,7 +73,12 @@ export default function ShopProductListRow({ listing, shopId }: ShopProductListR
|
||||
/>
|
||||
)}
|
||||
{discountPercent != null && (
|
||||
<DiscountBadge percent={discountPercent} className="absolute right-1 top-1" />
|
||||
<DiscountBadge percent={discountPercent} className="absolute left-1 top-1" />
|
||||
)}
|
||||
{isPendingApproval && (
|
||||
<span className="absolute right-1 top-1 flex h-[22px] w-[22px] items-center justify-center rounded-full bg-amber-600 text-white">
|
||||
<BoldIcon name="clock" size={14} tinted />
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -60,10 +60,11 @@ export function selectionCardClass(active: boolean, className?: string) {
|
||||
);
|
||||
}
|
||||
|
||||
/** Compact profile / grid action buttons */
|
||||
/** Compact profile / grid action buttons — گردیِ ۱۲px، عینِ actionBtn توی
|
||||
* ShopProfileScreen.tsx اندروید (borderRadius:12) */
|
||||
export const profileActionBtnClass = cn(
|
||||
btnDefault,
|
||||
"flex h-7 md:h-8 w-full max-w-full items-center justify-center rounded-3xl px-2 text-center text-[12px] font-semibold md:text-sm max-sm:text-[8px]"
|
||||
"flex h-7 md:h-8 w-full max-w-full items-center justify-center rounded-xl px-2 text-center text-[12px] font-semibold md:text-sm max-sm:text-[8px]"
|
||||
);
|
||||
|
||||
export const btnHeightSm = "h-9";
|
||||
|
||||
@@ -114,7 +114,12 @@ export interface IShopProductVariant {
|
||||
|
||||
export interface IShopProductListing {
|
||||
_id: string;
|
||||
shop: string | { _id: string; name: string; logo?: string | null };
|
||||
shop: string | {
|
||||
_id: string;
|
||||
name: string;
|
||||
logo?: string | null;
|
||||
owner?: { verify_badge?: string | null } | null;
|
||||
};
|
||||
catalogProduct: string | { _id: string; name: string; description?: string | null };
|
||||
title: string;
|
||||
description?: string | null;
|
||||
|
||||
Reference in New Issue
Block a user