diff --git a/package.json b/package.json index 2be1fd3..6100d0c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "modstagram-next", - "version": "4.2.0", + "version": "4.4.0", "private": true, "scripts": { "dev": "next dev --turbopack --port 3004", diff --git a/src/app/(auth)/(register)/register/complete/page.tsx b/src/app/(auth)/(register)/register/complete/page.tsx index bf4edb2..b4685d0 100644 --- a/src/app/(auth)/(register)/register/complete/page.tsx +++ b/src/app/(auth)/(register)/register/complete/page.tsx @@ -1,12 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ "use client"; -import AuthHead from "@/components/auth/AuthHead"; import AuthPageLayout, { AuthFormFooter, AuthPageContent, } from "@/components/auth/AuthPageLayout"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; +import Image from "next/image"; import useAxios from "@/hooks/useAxios"; import { useRouter } from "next/navigation"; import AuthNextButton from "@/components/auth/AuthNextButton"; @@ -14,6 +14,7 @@ import toast from "react-hot-toast"; import { getSafeRedirectPath } from "@/lib/auth/postLogin"; import { useTranslation } from "react-i18next"; import LocalePageShell from "@/components/i18n/LocalePageShell"; +import { IMAGE_BASE_URL } from "@/components/main/BaseUrl"; import { IProfileData } from "@/types/types"; function RegisterCompletePage() { @@ -21,6 +22,16 @@ function RegisterCompletePage() { const router = useRouter(); const { request, loading } = useAxios(); const [action, setAction] = useState<"app" | "continue" | null>(null); + const [user, setUser] = useState(null); + + useEffect(() => { + request<{ user: IProfileData }>("GET", "/profile") + .then((res) => setUser(res?.user ?? null)) + .catch(() => {}); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const name = [user?.first_name, user?.last_name].filter(Boolean).join(" "); // مطابق درخواست: عکس پروفایل حالا زودتر، توی خودِ صفحه‌ی register/avatar گرفته // می‌شه (قبل از این صفحه) — پس دیگه لازم نیست ویزارد تایید هویت با verify/avatar @@ -49,7 +60,10 @@ function RegisterCompletePage() { if (next === "continue") { await goToVerifyWizard(); } else { - router.push(getSafeRedirectPath()); + // navigation سخت — همون دلیلِ Logout.tsx: router.push ممکنه نسخه‌ی + // کش‌شده‌ی فید رو از زمانی که هنوز مهمان بودیم (قبل از تکمیلِ ثبت‌نام، + // ریدایرکت‌شده به ورود) دوباره نشون بده، بدون اجرای دوباره‌ی میدل‌ور + window.location.href = getSafeRedirectPath(); } } catch (err: any) { toast.error( @@ -65,32 +79,53 @@ function RegisterCompletePage() {
- -

+

+ + {name ? ( + + {name} + + ) : null} + {user?.user_name ? ( + {user.user_name} + ) : null} +
+

{t("auth.registerCompleteDesc")}

-
- finishRegistration("continue")} - className="!border-[#16a34a] !text-[#16a34a] w-full sm:w-auto" - disabled={loading} - loading={loading && action === "continue"} - > - {t("auth.continueRegistration")} - +
finishRegistration("app")} - className="!border-[#FF0000] !text-[#FF0000] w-full sm:w-auto" + className="!border-[#16a34a] !text-[#16a34a] flex-1" disabled={loading} loading={loading && action === "app"} > {t("auth.enterApp")} + finishRegistration("continue")} + className="!border-[#FF0000] !text-[#FF0000] flex-1" + disabled={loading} + loading={loading && action === "continue"} + > + {t("auth.continueRegistration")} +
diff --git a/src/app/(projects)/projects/[id]/[title]/page.tsx b/src/app/(projects)/projects/[id]/[title]/page.tsx index 0906883..329eda0 100644 --- a/src/app/(projects)/projects/[id]/[title]/page.tsx +++ b/src/app/(projects)/projects/[id]/[title]/page.tsx @@ -3,6 +3,7 @@ import { Suspense, cache } from "react"; import { cookies } from "next/headers"; import ProjectDetailClient from "./ProjectDetailClient"; import PageLoader from "@/components/ui/PageLoader"; +import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; import { fetchProject } from "@/api/fetchProject"; import { @@ -67,6 +68,7 @@ export default async function ProjectDetailPage({ }> + ); } diff --git a/src/app/billboards/[id]/[title]/page.tsx b/src/app/billboards/[id]/[title]/page.tsx index 95b7a2d..1fef6a1 100644 --- a/src/app/billboards/[id]/[title]/page.tsx +++ b/src/app/billboards/[id]/[title]/page.tsx @@ -1,5 +1,6 @@ import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl"; import BillboardDetailContent from "@/components/billboards/BillboardDetailContent"; +import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal"; import { IAdvertising, IRate } from "@/types/types"; import { cookies } from "next/headers"; import { cache } from "react"; @@ -135,6 +136,7 @@ async function BillboardPage({ params }: IBillboardProps) { }} /> + ); } diff --git a/src/app/explore/[id]/page.tsx b/src/app/explore/[id]/page.tsx index f5a880a..925e526 100644 --- a/src/app/explore/[id]/page.tsx +++ b/src/app/explore/[id]/page.tsx @@ -8,6 +8,7 @@ import { buildPostSeoTitle, } from "@/lib/postSlug"; import ExploreReelClient from "./ExploreReelClient"; +import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal"; import { getServerLanguage } from "@/lib/i18n/server"; import { translateCommon } from "@/lib/i18n/translate"; @@ -44,5 +45,10 @@ export async function generateMetadata({ params }: Props): Promise { export default async function ExploreReelPage({ params }: Props) { const { id } = await params; - return ; + return ( + <> + + + + ); } diff --git a/src/app/globals.css b/src/app/globals.css index 63f693f..7c24b49 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -182,6 +182,14 @@ html { scroll-behavior: smooth; + /* بدون این، Chrome روی گوشی‌هایی که «Force dark برای محتوای وب» روشنه (پیش‌فرض بعضی + بیلدهای شیائومی/سامسونگ) فکر می‌کنه این سایت از تم تیره پشتیبانی نمی‌کنه و خودش + رنگ‌های input/text رو الگوریتمی معکوس می‌کنه — دقیقاً همون چیزی که باعث می‌شد نام/ + نام‌خانوادگی توی تم تیره ناخوانا بشه. اعلامِ صریحِ color-scheme این رفتار رو خاموش می‌کنه */ + color-scheme: light; +} +html.dark { + color-scheme: dark; } body { direction: rtl; diff --git a/src/app/posts/[id]/[[...slug]]/page.tsx b/src/app/posts/[id]/[[...slug]]/page.tsx index c3704cc..ef5bc36 100644 --- a/src/app/posts/[id]/[[...slug]]/page.tsx +++ b/src/app/posts/[id]/[[...slug]]/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import { cache } from "react"; import { fetchPostById } from "@/api/fetchPostById"; import PostFeedView, { PostFeedSearchParams } from "@/components/posts/PostFeedView"; +import GuestAuthPromptModal from "@/components/auth/GuestAuthPromptModal"; import { buildStorageUrl } from "@/components/main/BaseUrl"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; import { @@ -125,6 +126,7 @@ export default async function PostPage({ params, searchParams }: Props) { showClose searchParams={feedSearchParams} /> + ); } diff --git a/src/components/VpnWarningGate.tsx b/src/components/VpnWarningGate.tsx index 14a36d9..e749b25 100644 --- a/src/components/VpnWarningGate.tsx +++ b/src/components/VpnWarningGate.tsx @@ -14,18 +14,24 @@ type GeoCheckResult = { isProxy: boolean; }; +const DISMISSED_KEY = "vpn_warning_dismissed"; + // عینِ VpnWarningGate.tsx اندروید: هر بار اپِ وب لود می‌شه (mount ریشه‌ی layout.tsx) // چک می‌شه که کاربر با آی‌پیِ غیرایران یا VPN/پراکسی وصل شده یا نه؛ اگه بله، هم یه // toast (معادلِ نوتیفِ اندروید — چون وب پرمیشنِ Notification API لازم داره) هم یه -// مودالِ غیراجباری نشون داده می‌شه +// مودالِ غیراجباری با دکمه‌ی تایید + تیکِ «دیگر نشان نده» کنارش نشون داده می‌شه. با +// زدنِ اون تیک، دیگه هیچ‌وقت (حتی خودِ چکِ شبکه‌ش) دوباره اجرا نمی‌شه export default function VpnWarningGate() { const { t } = useTranslation("common"); const [open, setOpen] = useState(false); + const [checked, setChecked] = useState(false); useEffect(() => { let cancelled = false; (async () => { try { + if (localStorage.getItem(DISMISSED_KEY) === "1") return; + const res = await apiClient.get( `${getApiBaseUrl()}/geo/check` ); @@ -42,20 +48,38 @@ export default function VpnWarningGate() { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + const close = () => { + setOpen(false); + if (checked) { + localStorage.setItem(DISMISSED_KEY, "1"); + } + }; + return ( - setOpen(false)} height="auto" elevated> +

{t("settings.vpnWarningTitle")}

{t("settings.vpnWarningMessage")}

- setOpen(false)} - > - {t("settings.vpnWarningAction")} - +
+ + {t("settings.vpnWarningAction")} + + +
); diff --git a/src/components/auth/GuestAuthPromptModal.tsx b/src/components/auth/GuestAuthPromptModal.tsx new file mode 100644 index 0000000..8b982ce --- /dev/null +++ b/src/components/auth/GuestAuthPromptModal.tsx @@ -0,0 +1,57 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useRouter } from "next/navigation"; +import { useTranslation } from "react-i18next"; +import Modal from "@/components/elements/Modal"; +import RoundedButton from "@/components/elements/RoundedButton"; +import { isAuthenticated } from "@/lib/auth/session"; + +// روی صفحه‌ی تکی‌ِ پست/ریلز/بیلبورد/پروژه (قابل‌اشتراک‌گذاری و قابل‌دیدن برای گوگل +// و مهمان، مثل اینستاگرام) — اگه کاربر لاگین نکرده، همین مودالِ پایین‌صفحه (قابل +// بستن) رو نشون بده با دو دکمه‌ی ورود/ثبت‌نام؛ خودِ محتوا و رفتارهایی مثل لایک/کامنت +// دست‌نخورده می‌مونه (لایک از قبل با useRequireAuth کاربرِ مهمان رو به ورود می‌فرسته) +export default function GuestAuthPromptModal() { + const { t } = useTranslation("common"); + const router = useRouter(); + const [open, setOpen] = useState(false); + + useEffect(() => { + if (!isAuthenticated()) setOpen(true); + }, []); + + const goTo = (path: "/login" | "/register") => { + const current = + typeof window !== "undefined" + ? window.location.pathname + window.location.search + : "/"; + setOpen(false); + router.push(`${path}?redirect=${encodeURIComponent(current)}`); + }; + + return ( + setOpen(false)} height="fit" elevated> +
+

{t("auth.guestPromptMessage")}

+
+ goTo("/login")} + > + {t("auth.guestPromptLogin")} + + goTo("/register")} + > + {t("auth.guestPromptRegister")} + +
+
+
+ ); +} diff --git a/src/components/settings/profile/AccountActionModals.tsx b/src/components/settings/profile/AccountActionModals.tsx index e2c2b0f..32bc54c 100644 --- a/src/components/settings/profile/AccountActionModals.tsx +++ b/src/components/settings/profile/AccountActionModals.tsx @@ -4,7 +4,6 @@ import { useEffect, useState } from "react"; import { motion, AnimatePresence } from "framer-motion"; import useAxios from "@/hooks/useAxios"; import toast from "react-hot-toast"; -import { useRouter } from "next/navigation"; import { btnPrimary, btnDefault } from "@/lib/ui/buttonStyles"; import { cn } from "@/lib/utils"; import { useTranslation } from "react-i18next"; @@ -27,7 +26,6 @@ export default function AccountActionModals({ }: AccountActionModalsProps) { const { t } = useTranslation("common"); const { request } = useAxios(); - const router = useRouter(); const [step, setStep] = useState("confirm"); const [typed, setTyped] = useState(""); const [loading, setLoading] = useState(false); @@ -59,7 +57,8 @@ export default function AccountActionModals({ await request("POST", "/account/delete", { user_name: typed.trim() }); await clearAuthSession(); toast.success(t("settings.accountActions.deleted")); - router.push("/login"); + // navigation سخت — همون دلیلِ Logout.tsx، کَشِ کلاینتیِ روتر رو دور می‌زنه + window.location.href = "/login"; } } catch (err) { toast.error( diff --git a/src/components/settings/settings/Logout.tsx b/src/components/settings/settings/Logout.tsx index f3d6b6e..1c1ff9d 100644 --- a/src/components/settings/settings/Logout.tsx +++ b/src/components/settings/settings/Logout.tsx @@ -1,5 +1,4 @@ "use client"; -import { useRouter } from "next/navigation"; import React, { useState } from "react"; import Modal from "../../elements/Modal"; import RoundedButton from "../../elements/RoundedButton"; @@ -10,11 +9,12 @@ import { useTranslation } from "react-i18next"; function Logout() { const { t } = useTranslation("common"); const [showModal, setShowModal] = useState(false); - const router = useRouter(); const logoutHandler = async () => { await clearAuthSession(); - router.push("/login"); - router.refresh(); + // router.push (soft navigation) می‌تونه کَشِ کلاینتیِ روتر (RSC payload قبلیِ + // صفحه‌ی اصلی، از وقتی لاگین بودیم) رو دوباره نشون بده بدون اینکه میدل‌ور + // دوباره اجرا بشه — navigation سخت، درخواست تازه به سرور می‌زنه + window.location.href = "/login"; }; return ( <> diff --git a/src/locales/en/common.json b/src/locales/en/common.json index 0caf9fd..60ea43c 100644 --- a/src/locales/en/common.json +++ b/src/locales/en/common.json @@ -215,8 +215,9 @@ }, "settings": { "vpnWarningTitle": "VPN usage detected", - "vpnWarningMessage": "For a better experience on the Modstagram platform, please turn off your VPN.", - "vpnWarningAction": "Got it", + "vpnWarningMessage": "Turn off your VPN for a better experience with Modstagram", + "vpnWarningAction": "Confirm", + "vpnWarningDontShowAgain": "Don't show again", "title": "Settings", "metaTitle": "Settings | Modstagram", "metaDescription": "Account settings on Modstagram", @@ -1183,6 +1184,9 @@ "savePasswordFailed": "Could not save password", "registerCompleteTitle": "Initial registration complete", "registerCompleteDesc": "You can enter the app or continue full registration (identity verification and profile).", + "guestPromptMessage": "Log in or sign up to see more and use all of Modstagram's features", + "guestPromptLogin": "Log in", + "guestPromptRegister": "Sign up", "identityVerification": "Identity verification", "forIdentityVerification": "For identity verification", "nationalCodePlaceholder": "National ID", diff --git a/src/locales/fa/common.json b/src/locales/fa/common.json index e551107..8fa4d61 100644 --- a/src/locales/fa/common.json +++ b/src/locales/fa/common.json @@ -215,8 +215,9 @@ }, "settings": { "vpnWarningTitle": "استفاده از VPN شناسایی شد", - "vpnWarningMessage": "برای تجربه بهتر از پلتفرم مدستاگرام، لطفاً VPN خود را خاموش کنید.", - "vpnWarningAction": "متوجه شدم", + "vpnWarningMessage": "برای تجربه بهتر از پلتفرم مدستاگرام VPN خود را خاموش کنید", + "vpnWarningAction": "تایید", + "vpnWarningDontShowAgain": "دیگر نشان نده", "title": "تنظیمات", "metaTitle": "تنظیمات | مدستاگرام", "metaDescription": "تنظیمات حساب کاربری در مدستاگرام", @@ -1183,6 +1184,9 @@ "savePasswordFailed": "ذخیره کلمه عبور ناموفق بود", "registerCompleteTitle": "ثبت‌نام اولیه تکمیل شد", "registerCompleteDesc": "می‌توانید وارد برنامه شوید یا ثبت‌نام کامل (تایید هویت و پروفایل) را ادامه دهید.", + "guestPromptMessage": "برای دیدن کامل و استفاده از امکانات مدستاگرام وارد شوید یا ثبت‌نام کنید", + "guestPromptLogin": "ورود", + "guestPromptRegister": "ثبت‌نام", "identityVerification": "احراز هویت", "forIdentityVerification": "جهت احراز هویت", "nationalCodePlaceholder": "کد ملی", diff --git a/src/middleware.ts b/src/middleware.ts index d81fa3f..3b680fc 100644 --- a/src/middleware.ts +++ b/src/middleware.ts @@ -1,82 +1,127 @@ -import { NextRequest, NextResponse } from "next/server"; -import { - buildLoginRedirect, - requiresRegistrationToken, -} from "@/lib/auth/registrationRoutes"; -import { LANGUAGE_COOKIE } from "@/lib/i18n/registry"; - -// فعلاً فقط en به‌صورت prefix مسیر پشتیبانی می‌شود؛ fa همچنان بدون prefix -// (روش سبک: بدون تغییر ساختار src/app، فقط rewrite داخلی + هدر زبان) -const LOCALE_PREFIXES = ["en"] as const; -type LocalePrefix = (typeof LOCALE_PREFIXES)[number]; - -function stripLocalePrefix(pathname: string): { - locale: LocalePrefix | null; - path: string; -} { - for (const locale of LOCALE_PREFIXES) { - if (pathname === `/${locale}`) return { locale, path: "/" }; - if (pathname.startsWith(`/${locale}/`)) { - return { locale, path: pathname.slice(locale.length + 1) }; - } - } - return { locale: null, path: pathname }; -} - -export function middleware(req: NextRequest) { - const isPrefetch = - req.headers.get("next-router-prefetch") === "1" || - req.headers.get("purpose") === "prefetch"; - - const { locale, path: pathname } = stripLocalePrefix(req.nextUrl.pathname); - const localePrefix = locale ? `/${locale}` : ""; - - // prefetch بدون کوکی نباید redirect به /login کش شود - if (!isPrefetch) { - const token = req.cookies.get("token")?.value?.trim(); - - // صفحاتی که ذاتاً نیازمند ورود هستند (حساب کاربری و ساخت محتوا) - const protectedPaths = ["/settings", "/new-post", "/new-project"]; - const needsAuth = protectedPaths.some((p) => pathname.startsWith(p)); - - if (needsAuth && !token) { - const loginUrl = new URL(`${localePrefix}/login`, req.url); - loginUrl.searchParams.set("redirect", pathname); - return NextResponse.redirect(loginUrl); - } - - // مراحل میانی ثبت‌نام بدون توکن → ورود - if (requiresRegistrationToken(pathname) && !token) { - return NextResponse.redirect( - new URL(`${localePrefix}${buildLoginRedirect(pathname)}`, req.url) - ); - } - } - - if (locale) { - const rewriteUrl = req.nextUrl.clone(); - rewriteUrl.pathname = pathname; - - const requestHeaders = new Headers(req.headers); - requestHeaders.set("x-app-language", locale); - - const res = NextResponse.rewrite(rewriteUrl, { - request: { headers: requestHeaders }, - }); - // برای اینکه ناوبری‌های بعدی (بدون prefix) هم انگلیسی بمونن، مثل انتخاب - // دستی زبان از سوییچر عمل می‌کنیم - res.cookies.set(LANGUAGE_COOKIE, locale, { - path: "/", - maxAge: 60 * 60 * 24 * 365, - sameSite: "lax", - }); - return res; - } - - return NextResponse.next(); -} - -export const config = { - matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"], -}; - \ No newline at end of file +import { NextRequest, NextResponse } from "next/server"; +import { + buildLoginRedirect, + requiresRegistrationToken, +} from "@/lib/auth/registrationRoutes"; +import { LANGUAGE_COOKIE } from "@/lib/i18n/registry"; + +// فعلاً فقط en به‌صورت prefix مسیر پشتیبانی می‌شود؛ fa همچنان بدون prefix +// (روش سبک: بدون تغییر ساختار src/app، فقط rewrite داخلی + هدر زبان) +const LOCALE_PREFIXES = ["en"] as const; +type LocalePrefix = (typeof LOCALE_PREFIXES)[number]; + +function stripLocalePrefix(pathname: string): { + locale: LocalePrefix | null; + path: string; +} { + for (const locale of LOCALE_PREFIXES) { + if (pathname === `/${locale}`) return { locale, path: "/" }; + if (pathname.startsWith(`/${locale}/`)) { + return { locale, path: pathname.slice(locale.length + 1) }; + } + } + return { locale: null, path: pathname }; +} + +// صفحاتی که کاربر مهمان (بدون توکن) همیشه باید بتونه ببینه: خودِ صفحات +// ورود/ثبت‌نام/قانونی، و callback اکانتِ گوگل/اینستاگرام +const PUBLIC_EXACT_PATHS = new Set([ + "/login", + "/login-with-username", + "/login-2fa", + "/forget-password", + "/forget-password-otp", + "/change-passowrd", + "/verify-otp", + "/register", + "/register-otp", + "/about-us", + "/privacy", + "/instagram-data-deletion", +]); + +const PUBLIC_PREFIXES = ["/auth/", "/sitemaps/", "/storage/"]; + +// صفحه‌ی تکی‌ِ یک پست/ریلز/بیلبورد/پروژه که به‌اشتراک گذاشته می‌شه — باید عین +// اینستاگرام برای گوگل و کاربرِ مهمان قابل‌دیدن بمونه (SEO + پیش‌نمایشِ لینکِ +// اشتراک‌گذاری)؛ خودِ مودالِ ورود/ثبت‌نام و قفل‌بودنِ اقدام‌هایی مثل لایک سمتِ +// کلاینت مدیریت می‌شه، نه اینجا +const RESERVED_FIRST_SEGMENT: Record> = { + billboards: new Set(["new", "payment", "profile"]), + projects: new Set(["new", "payment", "specialists"]), +}; + +function isSharedDetailPath(pathname: string, root: "billboards" | "projects"): boolean { + const match = pathname.match(new RegExp(`^/${root}/([^/]+)/([^/]+)$`)); + if (!match) return false; + return !RESERVED_FIRST_SEGMENT[root].has(match[1]); +} + +function isPublicGuestPath(pathname: string): boolean { + if (PUBLIC_EXACT_PATHS.has(pathname)) return true; + if (PUBLIC_PREFIXES.some((p) => pathname.startsWith(p))) return true; + if (/^\/posts\/[^/]+(\/.*)?$/.test(pathname)) return true; + if (/^\/explore\/[^/]+$/.test(pathname)) return true; + if (isSharedDetailPath(pathname, "billboards")) return true; + if (isSharedDetailPath(pathname, "projects")) return true; + return false; +} + +export function middleware(req: NextRequest) { + const isPrefetch = + req.headers.get("next-router-prefetch") === "1" || + req.headers.get("purpose") === "prefetch"; + + const { locale, path: pathname } = stripLocalePrefix(req.nextUrl.pathname); + const localePrefix = locale ? `/${locale}` : ""; + + // prefetch بدون کوکی نباید redirect به /login کش شود + if (!isPrefetch) { + const token = req.cookies.get("token")?.value?.trim(); + + if (!token) { + // مراحل میانی ثبت‌نام: نیاز به توکنِ مرحله‌ایِ خودِ ثبت‌نام (redirect با پارامترِ + // بازگشتِ دقیق به همون مرحله) + if (requiresRegistrationToken(pathname)) { + return NextResponse.redirect( + new URL(`${localePrefix}${buildLoginRedirect(pathname)}`, req.url) + ); + } + + // کاربر مهمان مستقیم وارد خودِ اپ نمی‌شه — اول باید ورود/ثبت‌نام کنه؛ فقط + // صفحاتِ عمومی (ورود/ثبت‌نام/قانونی) و صفحه‌ی تکی‌ِ اشتراک‌گذاری‌شده + // (پست/ریلز/بیلبورد/پروژه) از این قاعده مستثنی‌اند + if (!isPublicGuestPath(pathname)) { + const loginUrl = new URL(`${localePrefix}/login`, req.url); + loginUrl.searchParams.set("redirect", pathname); + return NextResponse.redirect(loginUrl); + } + } + } + + if (locale) { + const rewriteUrl = req.nextUrl.clone(); + rewriteUrl.pathname = pathname; + + const requestHeaders = new Headers(req.headers); + requestHeaders.set("x-app-language", locale); + + const res = NextResponse.rewrite(rewriteUrl, { + request: { headers: requestHeaders }, + }); + // برای اینکه ناوبری‌های بعدی (بدون prefix) هم انگلیسی بمونن، مثل انتخاب + // دستی زبان از سوییچر عمل می‌کنیم + res.cookies.set(LANGUAGE_COOKIE, locale, { + path: "/", + maxAge: 60 * 60 * 24 * 365, + sameSite: "lax", + }); + return res; + } + + return NextResponse.next(); +} + +export const config = { + matcher: ["/((?!api|_next/static|_next/image|favicon.ico|.*\\..*).*)"], +};