Initial commit

This commit is contained in:
payacom
2026-07-07 12:50:20 +03:30
parent 7af3af4e05
commit d234c7f859
31 changed files with 733 additions and 219 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@@ -1,12 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://modstagram.com/sitemap.xml</loc><lastmod>2026-07-02T19:42:50.918Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/robots.txt</loc><lastmod>2026-07-02T19:42:50.919Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/explore</loc><lastmod>2026-07-02T19:42:50.919Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/academy/payment/failed</loc><lastmod>2026-07-02T19:42:50.919Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/academy/payment/success</loc><lastmod>2026-07-02T19:42:50.919Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/about-us</loc><lastmod>2026-07-02T19:42:50.919Z</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://modstagram.com/robots.txt</loc><lastmod>2026-07-07T08:04:14.348Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/sitemap.xml</loc><lastmod>2026-07-07T08:04:14.349Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/explore</loc><lastmod>2026-07-07T08:04:14.350Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/academy/payment/failed</loc><lastmod>2026-07-07T08:04:14.350Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/academy/payment/success</loc><lastmod>2026-07-07T08:04:14.350Z</lastmod><changefreq>daily</changefreq><priority>0.7</priority></url>
<url><loc>https://modstagram.com/about-us</loc><lastmod>2026-07-07T08:04:14.350Z</lastmod><changefreq>monthly</changefreq><priority>0.8</priority></url>
<url><loc>https://modstagram.com</loc><changefreq>daily</changefreq><priority>1</priority></url>
<url><loc>https://modstagram.com/projects</loc><changefreq>daily</changefreq><priority>0.9</priority></url>
<url><loc>https://modstagram.com/billboards</loc><changefreq>daily</changefreq><priority>0.9</priority></url>
<url><loc>https://modstagram.com/billboards/6a4b9700a05fe9d26fa4312f/%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%20%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%AF%DA%A9%D8%AA%D8%B1%20%D8%B2%D8%A7%D8%B1%D8%A7</loc><changefreq>daily</changefreq><priority>0.8</priority></url>
<url><loc>https://modstagram.com/billboards/6a0ef05d12cadb57c40ee4b2/%D8%B9%DA%A9%D8%A7%D8%B3%DB%8C%20%D8%A8%D8%A8%D8%B1%DB%8C%20%D8%A2%D8%B1%D8%AA</loc><changefreq>daily</changefreq><priority>0.8</priority></url>
</urlset>

View File

@@ -41,12 +41,16 @@ export async function fetchBillboards(
Authorization: token ? `Bearer ${token}` : "",
},
});
} catch (error) {
console.error("[fetchBillboards] fetch failed:", apiUrl, error);
return { advertisings: [], totalItems: 0, totalPages: 0 };
} finally {
clearTimeout(timeoutId);
}
if (!response.ok) {
throw new Error("Network response was not ok");
console.error("[fetchBillboards] bad response:", response.status, apiUrl);
return { advertisings: [], totalItems: 0, totalPages: 0 };
}
return response.json();

View File

@@ -3,7 +3,7 @@
import AuthButton from "@/components/auth/AuthButton";
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import AuthPasswordInput from "@/components/auth/AuthPasswordInput";
import Container from "@/components/elements/Container";
import AuthRules from "@/components/auth/AuthRules";
import Link from "next/link";
@@ -71,9 +71,8 @@ function ChangePassword() {
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthInput
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور جدید"
className={`border mt-4 ${
formik.touched.password && formik.errors.password
@@ -90,9 +89,8 @@ function ChangePassword() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
formik.touched.confirmPassword && formik.errors.confirmPassword

View File

@@ -7,13 +7,14 @@ import Container from "@/components/elements/Container";
import AuthRules from "@/components/auth/AuthRules";
import Link from "next/link";
import React, { useState } from "react";
import OtpInput from "react-otp-input";
import AuthOtpInput from "@/components/auth/AuthOtpInput";
import AuthOtpStatus from "@/components/auth/AuthOtpStatus";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { IVerifyOtp } from "@/types/types";
import Cookies from "js-cookie";
import { setAuthSession } from "@/lib/auth/session";
// Validation schema using Yup
const schema = yup.object({
@@ -26,6 +27,8 @@ const schema = yup.object({
function ForgetPasswordOtp() {
const router = useRouter();
const [isModalOpen, setModalOpen] = useState(false);
const [otpExpired, setOtpExpired] = useState(false);
const [resendLoading, setResendLoading] = useState(false);
const { request, loading } = useAxios();
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
@@ -47,12 +50,7 @@ function ForgetPasswordOtp() {
mobile,
otp: values.otp.trim(),
})) as IVerifyOtp;
document.cookie = `token=${response.token}; path=/; Secure`;
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365
});
await setAuthSession(response.token);
await localStorage.setItem("otp", values.otp);
router.push("/change-passowrd");
} catch (err: any) {
@@ -65,31 +63,32 @@ function ForgetPasswordOtp() {
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="فراموش کردن کلمه عبور" />
<small className="mb-10">کد 6 رقمی به شماره {mobile} ارسال شد</small>
<AuthOtpStatus
mobile={mobile}
resendLoading={resendLoading}
onExpiredChange={setOtpExpired}
onResend={async () => {
if (!mobile) return;
setResendLoading(true);
try {
await request("POST", "/login", { mobile });
formik.setFieldValue("otp", "");
} finally {
setResendLoading(false);
}
}}
/>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<OtpInput
<AuthOtpInput
className="mt-3"
value={formik.values.otp}
onChange={(otp) => {
if (formik.values.otp !== otp) {
formik.setFieldValue("otp", otp);
if (otp.length === 6) {
setTimeout(() => {
formik.submitForm();
}, 500);
}
}
}}
numInputs={6}
containerStyle="mt-3 dir-ltr flex gap-x-4 justify-center"
inputStyle={`text-center min-w-[30px] border bg-secondary-light dark:bg-secondary-dark rounded-2xl py-3 ${
formik.touched.otp && formik.errors.otp
? "border-red-500 dark:border-red-500"
: "border-border-primary-light dark:border-border-primary-dark"
} focus:outline-none`}
renderInput={(props) => <input {...props} />}
onChange={(otp) => formik.setFieldValue("otp", otp)}
onComplete={() => setTimeout(() => formik.submitForm(), 400)}
error={Boolean(formik.touched.otp && formik.errors.otp)}
disabled={loading || otpExpired}
/>
{formik.touched.otp && formik.errors.otp && (
<small className="text-red-500 mt-2">{formik.errors.otp}</small>
@@ -97,7 +96,7 @@ function ForgetPasswordOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
</AuthButton>

View File

@@ -9,7 +9,7 @@ import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { markOtpSent } from "@/lib/auth/otpTimer";
// Validation schema using Yup
const schema = yup.object({
@@ -32,6 +32,7 @@ function ForgetPassword() {
try {
await request("POST", "/login", { mobile: values.mobile });
await localStorage.setItem("mobile", values.mobile);
markOtpSent();
router.push("/forget-password-otp");
} catch (err) {
console.log(err);

View File

@@ -4,6 +4,7 @@
import AuthButton from "@/components/auth/AuthButton";
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import AuthPasswordInput from "@/components/auth/AuthPasswordInput";
import Container from "@/components/elements/Container";
import AuthRules from "@/components/auth/AuthRules";
import Link from "next/link";
@@ -13,7 +14,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { IVerifyOtp } from "@/types/types";
import Cookies from "js-cookie";
import { setAuthSession } from "@/lib/auth/session";
// Validation schema using Yup
const schema = yup.object().shape({
@@ -50,27 +51,19 @@ function LoginWithUsername() {
})) as IVerifyOtp;
switch (response?.page) {
case "home":
document.cookie = `token=${response.token}; path=/; Secure`;
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365,
await setAuthSession(response.token, {
id: response.id,
user_type: response.user_type,
step: response.step,
});
await localStorage.setItem("usertype", response.user_type);
await localStorage.setItem("id", response.id);
await localStorage.setItem("step", response.step);
router.refresh();
router.push("/");
break;
case "auth-page":
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365,
await setAuthSession(response.token, {
id: response.id,
step: response.step,
});
// await localStorage.setItem("usertype", response.user_type);
await localStorage.setItem("id", response.id);
await localStorage.setItem("step", response.step);
router.push("/auth");
break;
default:
@@ -108,9 +101,8 @@ function LoginWithUsername() {
{formik.errors.username}
</small>
)}
<AuthInput
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور"
className={`border mt-4 ${
formik.touched.password && formik.errors.password

View File

@@ -10,7 +10,7 @@ import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { markOtpSent } from "@/lib/auth/otpTimer";
// Validation schema using Yup
const schema = yup.object({
@@ -34,6 +34,7 @@ function Login() {
try {
await request("POST", "/login", { mobile: values.mobile });
await localStorage.setItem("mobile", values.mobile);
markOtpSent();
router.push("/verify-otp");
} catch (err) {
console.log(err);

View File

@@ -7,13 +7,14 @@ import Container from "@/components/elements/Container";
import AuthRules from "@/components/auth/AuthRules";
import Link from "next/link";
import React, { useState } from "react";
import OtpInput from "react-otp-input";
import AuthOtpInput from "@/components/auth/AuthOtpInput";
import AuthOtpStatus from "@/components/auth/AuthOtpStatus";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { IVerifyOtp } from "@/types/types";
import Cookies from "js-cookie";
import { setAuthSession } from "@/lib/auth/session";
// Validation schema using Yup
const schema = yup.object({
@@ -26,6 +27,8 @@ const schema = yup.object({
function VerifyOtp() {
const router = useRouter();
const [isModalOpen, setModalOpen] = useState(false);
const [otpExpired, setOtpExpired] = useState(false);
const [resendLoading, setResendLoading] = useState(false);
const { request, loading } = useAxios();
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
@@ -50,25 +53,19 @@ function VerifyOtp() {
switch (response?.page) {
case "home":
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365,
await setAuthSession(response.token, {
id: response.id,
user_type: response.user_type,
step: response.step,
});
await localStorage.setItem("usertype", response.user_type);
await localStorage.setItem("id", response.id);
await localStorage.setItem("step", response.step);
router.refresh();
router.push("/");
break;
case "auth-page":
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365,
await setAuthSession(response.token, {
id: response.id,
step: response.step,
});
// await localStorage.setItem("usertype", response.user_type);
await localStorage.setItem("id", response.id);
await localStorage.setItem("step", response.step);
switch (response.step) {
case "user_name":
@@ -102,34 +99,32 @@ function VerifyOtp() {
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="ورود" />
<small className="mb-10">کد 6 رقمی به شماره {mobile} ارسال شد</small>
<AuthOtpStatus
mobile={mobile}
resendLoading={resendLoading}
onExpiredChange={setOtpExpired}
onResend={async () => {
if (!mobile) return;
setResendLoading(true);
try {
await request("POST", "/login", { mobile });
formik.setFieldValue("otp", "");
} finally {
setResendLoading(false);
}
}}
/>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<OtpInput
<AuthOtpInput
className="mt-3"
value={formik.values.otp}
onChange={(otp) => {
formik.setFieldValue("otp", otp);
if (otp.length === 6) {
setTimeout(() => formik.submitForm(), 500);
}
}}
numInputs={6}
containerStyle="mt-3 dir-ltr flex gap-x-4 justify-center"
inputStyle={`text-center min-w-[30px] border bg-secondary-light dark:bg-secondary-dark rounded-2xl py-3 ${
formik.touched.otp && formik.errors.otp
? "border-red-500 dark:border-red-500"
: "border-border-primary-light "
} focus:outline-none`}
renderInput={(props) => (
<input
{...props}
type="tel"
inputMode="numeric"
pattern="[0-9]*"
/>
)}
onChange={(otp) => formik.setFieldValue("otp", otp)}
onComplete={() => setTimeout(() => formik.submitForm(), 400)}
error={Boolean(formik.touched.otp && formik.errors.otp)}
disabled={loading || otpExpired}
/>
{formik.touched.otp && formik.errors.otp && (
@@ -138,7 +133,7 @@ function VerifyOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
</AuthButton>

View File

@@ -8,13 +8,14 @@ import Container from "@/components/elements/Container";
import AuthRules from "@/components/auth/AuthRules";
import Link from "next/link";
import React, { useState } from "react";
import OtpInput from "react-otp-input";
import AuthOtpInput from "@/components/auth/AuthOtpInput";
import AuthOtpStatus from "@/components/auth/AuthOtpStatus";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { IVerifyOtp } from "@/types/types";
import { useRouter } from "next/navigation";
import Cookies from "js-cookie";
import { setAuthSession } from "@/lib/auth/session";
// Validation schema using Yup
const schema = yup.object({
@@ -27,6 +28,8 @@ const schema = yup.object({
function RegisterOtp() {
const router = useRouter();
const [isModalOpen, setModalOpen] = useState(false);
const [otpExpired, setOtpExpired] = useState(false);
const [resendLoading, setResendLoading] = useState(false);
const { request, loading } = useAxios();
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
@@ -49,12 +52,7 @@ function RegisterOtp() {
otp: values.otp.trim(),
})) as IVerifyOtp;
await localStorage.setItem("otp", values.otp);
Cookies.set("token", response.token, {
secure: false,
path: "/",
expires: 365
});
await localStorage.setItem("step", response.step);
await setAuthSession(response.token, { step: response.step });
response.step === "user_name"
? router.push("/register/username")
: response.step === "password"
@@ -74,31 +72,32 @@ function RegisterOtp() {
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="ثبت نام" />
<small className="mb-10">کد 6 رقمی به شماره {mobile} ارسال شد</small>
<AuthOtpStatus
mobile={mobile}
resendLoading={resendLoading}
onExpiredChange={setOtpExpired}
onResend={async () => {
if (!mobile) return;
setResendLoading(true);
try {
await request("POST", "/register", { mobile });
formik.setFieldValue("otp", "");
} finally {
setResendLoading(false);
}
}}
/>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<OtpInput
<AuthOtpInput
className="mt-3"
value={formik.values.otp}
onChange={(otp) => {
if (formik.values.otp !== otp) {
formik.setFieldValue("otp", otp);
if (otp.length === 6) {
setTimeout(() => {
formik.submitForm();
}, 500);
}
}
}}
numInputs={6}
containerStyle="mt-3 dir-ltr flex gap-x-4 justify-center"
inputStyle={`text-center min-w-[30px] border bg-secondary-light dark:bg-secondary-dark rounded-2xl py-3 ${
formik.touched.otp && formik.errors.otp
? "border-red-500 dark:border-red-500"
: "border-border-primary-light dark:border-border-primary-dark"
} focus:outline-none`}
renderInput={(props) => <input {...props} />}
onChange={(otp) => formik.setFieldValue("otp", otp)}
onComplete={() => setTimeout(() => formik.submitForm(), 400)}
error={Boolean(formik.touched.otp && formik.errors.otp)}
disabled={loading || otpExpired}
/>
{formik.touched.otp && formik.errors.otp && (
<small className="text-red-500 mt-2">{formik.errors.otp}</small>
@@ -106,7 +105,7 @@ function RegisterOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
</AuthButton>

View File

@@ -9,7 +9,7 @@ import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { markOtpSent } from "@/lib/auth/otpTimer";
// Validation schema using Yup
const schema = yup.object({
@@ -33,6 +33,7 @@ function Register() {
try {
await request("POST", "/register", { mobile: values.mobile });
await localStorage.setItem("mobile", values.mobile);
markOtpSent();
router.push("/register-otp");
} catch (err) {
console.log(err);

View File

@@ -2,7 +2,7 @@
"use client";
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import AuthPasswordInput from "@/components/auth/AuthPasswordInput";
import Container from "@/components/elements/Container";
import React from "react";
import useAxios from "@/hooks/useAxios";
@@ -68,9 +68,8 @@ function PasswordPage() {
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthInput
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور"
className={`border mt-4 ${
formik.touched.password && formik.errors.password
@@ -87,9 +86,8 @@ function PasswordPage() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
formik.touched.confirmPassword && formik.errors.confirmPassword

View File

@@ -4,27 +4,21 @@
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import Container from "@/components/elements/Container";
import React from "react";
import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { IVerifyOtp } from "@/types/types";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
// ✅ فقط چک می‌کنیم مقدار وارد شده باشه
const schema = yup.object().shape({
username: yup
.string()
.required("نام کاربری الزامی است")
.min(1, "نام کاربری باید حداقل ۱ کاراکتر باشد")
.max(100, "نام کاربری نمی‌تواند بیشتر از ۱۰۰ کاراکتر باشد"),
});
import UsernameSuggestions from "@/components/auth/UsernameSuggestions";
import { usernameFormSchema } from "@/lib/validation/usernameSchema";
import { sanitizeUsernameInput } from "@/lib/validation/username";
function UsernamePage() {
const router = useRouter();
const { request, loading } = useAxios();
const [duplicateError, setDuplicateError] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<string[]>([]);
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
@@ -32,27 +26,42 @@ function UsernamePage() {
initialValues: {
username: "",
},
validationSchema: schema,
validationSchema: usernameFormSchema,
onSubmit: async (values) => {
setDuplicateError(null);
setSuggestions([]);
try {
await request("POST", "/register/username", {
mobile,
user_name: values.username,
user_name: values.username.trim().toLowerCase(),
}) as IVerifyOtp;
localStorage.setItem("username", values.username);
localStorage.setItem("username", values.username.trim().toLowerCase());
router.push("/register/password");
} catch (err: any) {
console.log("Unhandled error:", err?.message);
if (err?.response?.data?.message) {
toast.error(err.response.data.message);
} else {
toast.error("خطایی رخ داد، دوباره تلاش کنید");
const data = err?.response?.data;
if (data?.message) {
setDuplicateError(data.message);
setSuggestions(Array.isArray(data.suggestions) ? data.suggestions : []);
}
}
},
});
const handleUsernameChange = (value: string) => {
setDuplicateError(null);
setSuggestions([]);
formik.setFieldValue("username", sanitizeUsernameInput(value));
};
const handleSuggestionSelect = (username: string) => {
formik.setFieldValue("username", username);
formik.setFieldTouched("username", true, false);
setDuplicateError(null);
setSuggestions([]);
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
@@ -64,23 +73,37 @@ function UsernamePage() {
<AuthInput
name="username"
type="text"
placeholder="نام کاربری خود را وارد کنید"
dir="auto" // ✅ اجازه ورود همه کاراکترها (فارسی، انگلیسی، ایموجی...)
placeholder="example_user"
dir="ltr"
autoComplete="username"
spellCheck={false}
className={`border ${
formik.touched.username && formik.errors.username
(formik.touched.username && formik.errors.username) || duplicateError
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.username}
onChange={formik.handleChange}
onChange={(e) => handleUsernameChange(e.target.value)}
onBlur={formik.handleBlur}
/>
{formik.touched.username && formik.errors.username && (
<small className="text-gray-500 dark:text-gray-400 text-xs mt-2 text-center leading-5">
فقط حروف انگلیسی، اعداد و کاراکترهای . _ - (حداقل ۶ کاراکتر)
</small>
{formik.touched.username && formik.errors.username && !duplicateError && (
<small className="text-red-500 mt-2 block">
{formik.errors.username}
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
<UsernameSuggestions
message={duplicateError || undefined}
suggestions={suggestions}
onSelect={handleSuggestionSelect}
/>
<AuthNextButton
type="submit"
className="mt-20"
disabled={loading || formik.values.username.length < 6}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
</AuthNextButton>
</form>

View File

@@ -0,0 +1,40 @@
import { cookies } from "next/headers";
import { NextResponse } from "next/server";
const TOKEN_KEY = "token";
const MAX_AGE = 365 * 24 * 60 * 60;
function cookieOptions() {
return {
path: "/",
maxAge: MAX_AGE,
sameSite: "lax" as const,
secure: process.env.NODE_ENV === "production",
httpOnly: false,
};
}
export async function POST(request: Request) {
try {
const body = await request.json();
const token = typeof body?.token === "string" ? body.token.trim() : "";
if (!token) {
return NextResponse.json({ error: "token required" }, { status: 400 });
}
const cookieStore = await cookies();
cookieStore.set(TOKEN_KEY, token, cookieOptions());
return NextResponse.json({ ok: true });
} catch {
return NextResponse.json({ error: "invalid request" }, { status: 400 });
}
}
export async function DELETE() {
const cookieStore = await cookies();
cookieStore.delete({ name: TOKEN_KEY, path: "/" });
return NextResponse.json({ ok: true });
}

View File

@@ -2,7 +2,7 @@
"use client";
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import AuthPasswordInput from "@/components/auth/AuthPasswordInput";
import Container from "@/components/elements/Container";
import React from "react";
import useAxios from "@/hooks/useAxios";
@@ -64,9 +64,8 @@ function PasswordPage() {
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthInput
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور"
className={`border mt-4 ${
formik.touched.password && formik.errors.password
@@ -83,9 +82,8 @@ function PasswordPage() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
formik.touched.confirmPassword && formik.errors.confirmPassword

View File

@@ -1,8 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import React, { useEffect } from "react";
import React, { useState } from "react";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import useAxios from "@/hooks/useAxios";
import { useUser } from "@/hooks/useUser";
@@ -10,41 +9,55 @@ import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import AuthNextButton from "@/components/auth/AuthNextButton";
import Container from "@/components/elements/Container";
// Validation schema using Yup
const schema = yup.object().shape({
username: yup.string().required("نام کاربری الزامی است"),
});
import UsernameSuggestions from "@/components/auth/UsernameSuggestions";
import { usernameFormSchema } from "@/lib/validation/usernameSchema";
import { sanitizeUsernameInput } from "@/lib/validation/username";
function UsernamePage() {
const router = useRouter();
const user = useUser();
const { request, loading } = useAxios();
const [duplicateError, setDuplicateError] = useState<string | null>(null);
const [suggestions, setSuggestions] = useState<string[]>([]);
const formik = useFormik({
initialValues: {
username: user?.user_name || "",
},
validationSchema: schema,
validationSchema: usernameFormSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setDuplicateError(null);
setSuggestions([]);
try {
await request("PATCH", "/register/username", {
user_name: values.username,
user_name: values.username.trim().toLowerCase(),
});
localStorage.setItem("username", values.username);
localStorage.setItem("username", values.username.trim().toLowerCase());
router.push("/settings/edit");
} catch (err: any) {
console.log("Unhandled error:", err?.message);
const data = err?.response?.data;
if (data?.message) {
setDuplicateError(data.message);
setSuggestions(Array.isArray(data.suggestions) ? data.suggestions : []);
}
}
},
});
// آپدیت مقدار username در صورتی که user.user_name مقدار داشته باشد
useEffect(() => {
if (user?.user_name) {
formik.setValues({ username: user.user_name });
}
}, [user?.user_name]);
const handleUsernameChange = (value: string) => {
setDuplicateError(null);
setSuggestions([]);
formik.setFieldValue("username", sanitizeUsernameInput(value));
};
const handleSuggestionSelect = (username: string) => {
formik.setFieldValue("username", username);
formik.setFieldTouched("username", true, false);
setDuplicateError(null);
setSuggestions([]);
};
return (
<Container>
@@ -57,22 +70,37 @@ function UsernamePage() {
<AuthInput
name="username"
type="text"
placeholder="نام کاربری خود را وارد کنید"
placeholder="example_user"
dir="ltr"
autoComplete="username"
spellCheck={false}
className={`border ${
formik.touched.username && formik.errors.username
(formik.touched.username && formik.errors.username) || duplicateError
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.username}
onChange={formik.handleChange}
onChange={(e) => handleUsernameChange(e.target.value)}
onBlur={formik.handleBlur}
/>
{formik.touched.username && formik.errors.username && (
<small className="text-gray-500 dark:text-gray-400 text-xs mt-2 text-center leading-5">
فقط حروف انگلیسی، اعداد و کاراکترهای . _ - (حداقل ۶ کاراکتر)
</small>
{formik.touched.username && formik.errors.username && !duplicateError && (
<small className="text-red-500 mt-2 block">
{formik.errors.username}
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
<UsernameSuggestions
message={duplicateError || undefined}
suggestions={suggestions}
onSelect={handleSuggestionSelect}
/>
<AuthNextButton
type="submit"
className="mt-20"
disabled={loading || formik.values.username.length < 6}
>
{loading ? "در حال ارسال..." : "ویرایش"}
</AuthNextButton>
</form>

View File

@@ -2,16 +2,23 @@
import { useEffect } from "react";
import { useRouter } from "next/navigation";
import { isAuthenticated } from "@/lib/auth/session";
const ROUTES = ["/academy", "/billboards", "/settings", "/explore"];
const PUBLIC_ROUTES = ["/academy", "/billboards", "/explore"];
export default function BackgroundPrefetch() {
const router = useRouter();
useEffect(() => {
const t = window.setTimeout(() => {
ROUTES.forEach((route) => router.prefetch(route));
PUBLIC_ROUTES.forEach((route) => router.prefetch(route));
// settings فقط وقتی لاگین است prefetch شود
if (isAuthenticated()) {
router.prefetch("/settings");
}
}, 2000);
return () => window.clearTimeout(t);
}, [router]);

View File

@@ -1,9 +1,20 @@
import React, { ComponentProps } from "react";
import { cn } from "@/lib/utils";
type TInput = ComponentProps<"input"> & {};
function AuthInput({ style, className, ...rest }: TInput) {
return (
<input
className={`w-full dir-ltr max-w-[290px] p-3 text-center rounded-2xl border border-border-secondary-light dark:border-border-secondary-dark bg-secondary-light dark:bg-secondary-dark font-medium ${className}`}
className={cn(
"w-full dir-ltr max-w-[290px] p-3.5 text-center text-lg font-semibold rounded-2xl",
"border-2 border-gray-200 dark:border-gray-300",
"bg-white text-gray-900 placeholder:text-gray-400",
"dark:bg-white dark:text-gray-900 dark:placeholder:text-gray-500",
"focus:outline-none focus:border-[#387E65] focus:ring-2 focus:ring-[#387E65]/30",
"shadow-sm transition-[border-color,box-shadow] duration-200",
className
)}
style={{ ...style }}
{...rest}
/>

View File

@@ -0,0 +1,82 @@
"use client";
import OtpInput from "react-otp-input";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
interface AuthOtpInputProps {
value: string;
onChange: (otp: string) => void;
onComplete?: (otp: string) => void;
numInputs?: number;
error?: boolean;
disabled?: boolean;
className?: string;
}
const cellBase =
"w-11 h-14 sm:w-12 sm:h-14 min-w-[44px] text-center text-2xl font-bold tabular-nums rounded-2xl border-[2.5px] bg-white text-gray-900 shadow-md outline-none caret-[#387E65] selection:bg-sky-200 dark:bg-white dark:text-gray-900 dark:caret-[#387E65]";
export default function AuthOtpInput({
value,
onChange,
onComplete,
numInputs = 6,
error = false,
disabled = false,
className,
}: AuthOtpInputProps) {
return (
<div className={cn("w-full flex justify-center", className)}>
<OtpInput
value={value}
onChange={(otp) => {
onChange(otp);
if (otp.length === numInputs) {
onComplete?.(otp);
}
}}
numInputs={numInputs}
inputType="tel"
shouldAutoFocus
skipDefaultStyles
containerStyle={{
display: "flex",
direction: "ltr",
gap: "0.5rem",
justifyContent: "center",
width: "100%",
}}
renderInput={(inputProps, index) => {
const filled = Boolean(inputProps.value);
return (
<motion.input
{...inputProps}
disabled={disabled}
animate={
filled
? { scale: [0.88, 1.1, 1], y: [6, -2, 0], opacity: 1 }
: { scale: 1, y: 0, opacity: 1 }
}
transition={{ duration: 0.28, ease: [0.22, 1, 0.36, 1] }}
className={cn(
cellBase,
filled
? "border-[#387E65] shadow-lg ring-2 ring-[#387E65]/25"
: "border-gray-500 dark:border-gray-400 shadow-md",
error && "border-red-500 ring-red-500/30",
"focus:border-[#387E65] focus:ring-2 focus:ring-[#387E65]/40 focus:shadow-lg",
disabled && "opacity-50 cursor-not-allowed"
)}
inputMode="numeric"
pattern="[0-9]*"
autoComplete={index === 0 ? "one-time-code" : "off"}
aria-label={`رقم ${index + 1} از ${numInputs}`}
/>
);
}}
/>
</div>
);
}

View File

@@ -0,0 +1,87 @@
"use client";
import { useEffect, useState } from "react";
import {
formatOtpCountdown,
getOtpRemainingSeconds,
markOtpSent,
OTP_VALID_SECONDS,
} from "@/lib/auth/otpTimer";
import { cn } from "@/lib/utils";
interface AuthOtpStatusProps {
mobile: string | null;
onResend: () => Promise<void>;
resendLoading?: boolean;
className?: string;
onExpiredChange?: (expired: boolean) => void;
}
export default function AuthOtpStatus({
mobile,
onResend,
resendLoading = false,
className,
onExpiredChange,
}: AuthOtpStatusProps) {
const [remaining, setRemaining] = useState(OTP_VALID_SECONDS);
const expired = remaining <= 0;
useEffect(() => {
setRemaining(getOtpRemainingSeconds());
const interval = window.setInterval(() => {
setRemaining(getOtpRemainingSeconds());
}, 1000);
return () => window.clearInterval(interval);
}, []);
useEffect(() => {
onExpiredChange?.(expired);
}, [expired, onExpiredChange]);
const handleResend = async () => {
await onResend();
markOtpSent();
setRemaining(OTP_VALID_SECONDS);
};
if (!mobile) return null;
return (
<div className={cn("w-full max-w-sm text-center mb-6 space-y-2", className)}>
<p className="text-sm sm:text-base font-medium text-gray-800 dark:text-gray-100 leading-relaxed">
کد ۶ رقمی به شماره{" "}
<span dir="ltr" className="font-bold text-[#387E65] inline-block">
{mobile}
</span>{" "}
ارسال شد
</p>
{!expired ? (
<p className="text-sm text-gray-600 dark:text-gray-300">
اعتبار کد:{" "}
<span
dir="ltr"
className="font-bold tabular-nums text-[#FF5C00] text-base"
>
{formatOtpCountdown(remaining)}
</span>
</p>
) : (
<div className="space-y-2">
<p className="text-sm font-medium text-red-600 dark:text-red-400">
کد منقضی شده است. لطفاً دوباره درخواست ارسال کد دهید.
</p>
<button
type="button"
onClick={handleResend}
disabled={resendLoading}
className="text-sm font-bold text-[#667AC1] hover:text-[#0033EA] disabled:opacity-50"
>
{resendLoading ? "در حال ارسال..." : "ارسال مجدد کد"}
</button>
</div>
)}
</div>
);
}

View File

@@ -0,0 +1,54 @@
"use client";
import React, { ComponentProps, useState } from "react";
import Image from "next/image";
import { cn } from "@/lib/utils";
type AuthPasswordInputProps = Omit<ComponentProps<"input">, "type"> & {
wrapperClassName?: string;
};
function AuthPasswordInput({
style,
className,
wrapperClassName,
...rest
}: AuthPasswordInputProps) {
const [visible, setVisible] = useState(false);
return (
<div className={cn("relative w-full max-w-[290px]", wrapperClassName)}>
<input
type={visible ? "text" : "password"}
className={cn(
"w-full dir-ltr p-3.5 px-12 text-center text-lg font-semibold rounded-2xl",
"border-2 border-gray-200 dark:border-gray-300",
"bg-white text-gray-900 placeholder:text-gray-400",
"dark:bg-white dark:text-gray-900 dark:placeholder:text-gray-500",
"focus:outline-none focus:border-[#387E65] focus:ring-2 focus:ring-[#387E65]/30",
"shadow-sm transition-[border-color,box-shadow] duration-200",
className
)}
style={{ ...style }}
{...rest}
/>
<button
type="button"
onClick={() => setVisible((prev) => !prev)}
className="absolute right-3 top-1/2 -translate-y-1/2 p-1 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-200 transition-colors"
aria-label={visible ? "مخفی کردن کلمه عبور" : "نمایش کلمه عبور"}
tabIndex={-1}
>
<Image
src={visible ? "/images/icons/eye-slash.png" : "/images/icons/eye-open.png"}
alt=""
width={22}
height={22}
className="opacity-80"
/>
</button>
</div>
);
}
export default AuthPasswordInput;

View File

@@ -0,0 +1,48 @@
"use client";
import { cn } from "@/lib/utils";
interface UsernameSuggestionsProps {
message?: string;
suggestions?: string[];
onSelect: (username: string) => void;
className?: string;
}
export default function UsernameSuggestions({
message,
suggestions = [],
onSelect,
className,
}: UsernameSuggestionsProps) {
if (!message && suggestions.length === 0) return null;
return (
<div className={cn("w-full max-w-sm mt-3 space-y-3", className)}>
{message && (
<p className="text-red-500 text-sm text-center font-medium">{message}</p>
)}
{suggestions.length > 0 && (
<div className="space-y-2">
<p className="text-xs text-gray-600 dark:text-gray-300 text-center">
پیشنهادهای در دسترس:
</p>
<div className="flex flex-wrap justify-center gap-2">
{suggestions.map((item) => (
<button
key={item}
type="button"
onClick={() => onSelect(item)}
dir="ltr"
className="px-3 py-2 rounded-xl border-2 border-[#387E65]/40 bg-white dark:bg-white text-[#387E65] text-sm font-semibold hover:bg-[#387E65]/10 hover:border-[#387E65] transition-colors"
>
{item}
</button>
))}
</div>
</div>
)}
</div>
);
}

View File

@@ -1,10 +1,15 @@
// لوکال: NEXT_PUBLIC_BASE_URL=http://localhost:3002/api/v1
// export const SOCKET_URL = "http://localhost:8000";
// export const BASE_URL = "http://localhost:8000/api/v1";
// export const IMAGE_BASE_URL = "http://localhost:8000/storage";
export const SOCKET_URL = "https://api.modstagram.com";
export const BASE_URL = "https://api.modstagram.com/api/v1";
export const IMAGE_BASE_URL = "https://api.modstagram.com/storage";
/** app.modstagram.ir — SSL معتبر برای fetch سمت سرور Next.js */
export const SOCKET_URL =
process.env.NEXT_PUBLIC_SOCKET_URL ?? "https://app.modstagram.ir";
export const BASE_URL =
process.env.NEXT_PUBLIC_BASE_URL ?? "https://app.modstagram.ir/api/v1";
export const IMAGE_BASE_URL =
process.env.NEXT_PUBLIC_IMAGE_BASE_URL ?? "https://app.modstagram.ir/storage";
/** ساخت URL کامل فایل storage — مسیرهای DB را یکسان می‌کند */
export function buildStorageUrl(path: string | null | undefined): string {

View File

@@ -1,26 +1,18 @@
"use client";
import Cookies from "js-cookie";
import Image from "next/image";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import Modal from "../../elements/Modal";
import RoundedButton from "../../elements/RoundedButton";
import { clearAuthSession } from "@/lib/auth/session";
function Logout() {
const [showModal, setShowModal] = useState<boolean>(false);
const router = useRouter();
const logoutHandler = () => {
localStorage.removeItem("id");
localStorage.removeItem("mobile");
localStorage.removeItem("otp");
localStorage.removeItem("token");
localStorage.removeItem("usertype");
localStorage.removeItem("first_name");
localStorage.removeItem("last_name");
localStorage.removeItem("username");
localStorage.removeItem("step");
Cookies.remove("token");
const logoutHandler = async () => {
await clearAuthSession();
router.push("/login");
router.refresh();
};
return (
<>

View File

@@ -11,6 +11,7 @@ import axios, {
import toast from "react-hot-toast";
import { BASE_URL as base } from "@/components/main/BaseUrl";
import Cookies from "js-cookie";
import { clearAuthSession } from "@/lib/auth/session";
const BASE_URL = base;
const axiosInstance = axios.create({
@@ -88,9 +89,14 @@ axiosInstance.interceptors.response.use(
);
const handleUnauthorized = (): void => {
if (typeof window !== "undefined") {
// toast.error("برای انجام عملیات لطفا وارد حساب کاربری خود شوید");
}
if (typeof window === "undefined") return;
const path = window.location.pathname;
if (path.startsWith("/login") || path.startsWith("/register")) return;
clearAuthSession().finally(() => {
window.location.href = "/login";
});
};
// تایپ اصلاح‌شده برای درخواست‌ها

23
src/lib/auth/otpTimer.ts Normal file
View File

@@ -0,0 +1,23 @@
export const OTP_VALID_SECONDS = 3 * 60;
const OTP_SENT_AT_KEY = "otp_sent_at";
export function markOtpSent(): void {
if (typeof window === "undefined") return;
sessionStorage.setItem(OTP_SENT_AT_KEY, Date.now().toString());
}
export function getOtpRemainingSeconds(): number {
if (typeof window === "undefined") return OTP_VALID_SECONDS;
const sent = sessionStorage.getItem(OTP_SENT_AT_KEY);
if (!sent) return OTP_VALID_SECONDS;
const elapsed = Math.floor((Date.now() - Number(sent)) / 1000);
return Math.max(0, OTP_VALID_SECONDS - elapsed);
}
export function formatOtpCountdown(totalSeconds: number): string {
const minutes = Math.floor(totalSeconds / 60);
const seconds = totalSeconds % 60;
return `${minutes}:${seconds.toString().padStart(2, "0")}`;
}

70
src/lib/auth/session.ts Normal file
View File

@@ -0,0 +1,70 @@
import Cookies from "js-cookie";
const TOKEN_KEY = "token";
export type AuthUserMeta = {
id?: string;
user_type?: string;
step?: string;
};
const cookieOptions = (): Cookies.CookieAttributes => ({
path: "/",
expires: 365,
sameSite: "Lax",
secure: typeof window !== "undefined" && window.location.protocol === "https:",
});
/** ذخیره توکن در کوکی مرورگر + Route Handler سرور (برای middleware) */
export async function setAuthSession(
token: string,
meta?: AuthUserMeta
): Promise<void> {
if (!token) return;
Cookies.set(TOKEN_KEY, token, cookieOptions());
await fetch("/api/auth/session", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token }),
credentials: "same-origin",
});
if (meta?.id) localStorage.setItem("id", meta.id);
if (meta?.user_type) localStorage.setItem("usertype", meta.user_type);
if (meta?.step) localStorage.setItem("step", meta.step);
}
/** پاک کردن کامل session */
export async function clearAuthSession(): Promise<void> {
Cookies.remove(TOKEN_KEY, { path: "/" });
try {
await fetch("/api/auth/session", {
method: "DELETE",
credentials: "same-origin",
});
} catch {
// ignore
}
localStorage.removeItem("id");
localStorage.removeItem("mobile");
localStorage.removeItem("otp");
localStorage.removeItem("token");
localStorage.removeItem("usertype");
localStorage.removeItem("first_name");
localStorage.removeItem("last_name");
localStorage.removeItem("username");
localStorage.removeItem("step");
}
export function getAuthToken(): string | undefined {
if (typeof window === "undefined") return undefined;
return Cookies.get(TOKEN_KEY);
}
export function isAuthenticated(): boolean {
return Boolean(getAuthToken());
}

View File

@@ -0,0 +1,21 @@
/** حروف انگلیسی، اعداد و . _ - — بدون فارسی و کاراکترهای غیرمعمول */
export const USERNAME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9._-]{5,99}$/;
export const USERNAME_MIN_LENGTH = 6;
export const USERNAME_MAX_LENGTH = 100;
export const USERNAME_ALLOWED_CHARS = /[^a-zA-Z0-9._-]/g;
export const USERNAME_VALIDATION_MESSAGE =
"نام کاربری فقط می‌تواند شامل حروف انگلیسی، اعداد و کاراکترهای . _ - باشد";
export const USERNAME_START_MESSAGE =
"نام کاربری باید با حرف یا عدد انگلیسی شروع شود";
export function sanitizeUsernameInput(value: string): string {
return value.replace(USERNAME_ALLOWED_CHARS, "");
}
export function isValidUsername(value: string): boolean {
return USERNAME_REGEX.test(value);
}

View File

@@ -0,0 +1,20 @@
import * as yup from "yup";
import {
USERNAME_MAX_LENGTH,
USERNAME_MIN_LENGTH,
USERNAME_REGEX,
USERNAME_START_MESSAGE,
USERNAME_VALIDATION_MESSAGE,
} from "./username";
export const usernameSchema = yup
.string()
.required("نام کاربری الزامی است")
.min(USERNAME_MIN_LENGTH, `نام کاربری باید حداقل ${USERNAME_MIN_LENGTH} کاراکتر باشد`)
.max(USERNAME_MAX_LENGTH, `نام کاربری نمی‌تواند بیشتر از ${USERNAME_MAX_LENGTH} کاراکتر باشد`)
.matches(/^[a-zA-Z0-9]/, USERNAME_START_MESSAGE)
.matches(USERNAME_REGEX, USERNAME_VALIDATION_MESSAGE);
export const usernameFormSchema = yup.object({
username: usernameSchema,
});

View File

@@ -1,17 +1,26 @@
import { NextRequest, NextResponse } from 'next/server';
import { NextRequest, NextResponse } from "next/server";
export function middleware(req: NextRequest) {
const token = req.cookies.get('token'); // دریافت توکن از کوکی
const isPrefetch =
req.headers.get("next-router-prefetch") === "1" ||
req.headers.get("purpose") === "prefetch";
// بررسی اینکه مسیر داخل settings هست
if (req.nextUrl.pathname.startsWith('/settings') && !token) {
return NextResponse.redirect(new URL('/login', req.url)); // انتقال به صفحه لاگین
// prefetch بدون کوکی نباید redirect به /login کش شود
if (isPrefetch) {
return NextResponse.next();
}
return NextResponse.next(); // ادامه پردازش عادی
const token = req.cookies.get("token")?.value?.trim();
if (req.nextUrl.pathname.startsWith("/settings") && !token) {
const loginUrl = new URL("/login", req.url);
loginUrl.searchParams.set("redirect", req.nextUrl.pathname);
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
// تعیین مسیرهایی که این middleware روی آنها اعمال می‌شود
export const config = {
matcher: ['/settings/:path*'], // فقط روی مسیرهای settings اعمال شود
matcher: ["/settings/:path*"],
};