Compare commits

...

3 Commits

Author SHA1 Message Date
payacom
378cf86fa6 updata 2026-07-07 15:01:04 +03:30
payacom
d234c7f859 Initial commit 2026-07-07 12:50:20 +03:30
payacom
7af3af4e05 Initial commit 2026-07-07 11:17:33 +03:30
102 changed files with 1529 additions and 843 deletions

View File

@@ -13,13 +13,25 @@ const nextConfig = {
remotePatterns: [
{
protocol: 'https',
hostname: 'app.modstagram.ir',
hostname: 'api.modstagram.com',
port: '',
pathname: '/**',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'blog.modstagram.com', // اجازه دادن به نمایش تصاویر وبلاگ
hostname: 'cdn.modstagram.com',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'app.modstagram.ir',
port: '',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'blog.modstagram.com',
port: '',
pathname: '/**',
},

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,11 +71,11 @@ 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 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.password && formik.errors.password
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -90,11 +90,11 @@ function ChangePassword() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.confirmPassword && formik.errors.confirmPassword
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -109,8 +109,8 @@ function ChangePassword() {
</small>
)}
<AuthButton type="submit" className="mt-5" disabled={loading}>
{loading ? "در حال ارسال..." : "تغییر کلمه عبور"}
<AuthButton type="submit" className="mt-5" loading={loading} disabled={loading}>
تغییر کلمه عبور
</AuthButton>
</form>

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,9 +96,9 @@ function ForgetPasswordOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
loading={loading} disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
تایید کد
</AuthButton>
</form>
<Link href={"/login-with-username"}>

View File

@@ -6,10 +6,11 @@ import AuthInput from "@/components/auth/AuthInput";
import Container from "@/components/elements/Container";
import Link from "next/link";
import React from "react";
import { useRouter } from "next/navigation";
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 +33,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);
@@ -51,6 +53,7 @@ function ForgetPassword() {
name="mobile"
maxLength={11}
type="text"
dir="ltr"
placeholder="09 _ _ _ _ _ _ _ _ _"
className={`border ${
formik.touched.mobile && formik.errors.mobile
@@ -66,8 +69,8 @@ function ForgetPassword() {
{formik.errors.mobile}
</small>
)}
<AuthButton type="submit" className="mt-5" disabled={loading}>
{loading ? "در حال ارسال..." : "ارسال کد"}
<AuthButton type="submit" className="mt-5" loading={loading} disabled={loading}>
ارسال کد
</AuthButton>
</form>
<Link href={"/register"}>

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,11 +101,11 @@ function LoginWithUsername() {
{formik.errors.username}
</small>
)}
<AuthInput
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور"
className={`border mt-4 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.password && formik.errors.password
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -126,8 +119,8 @@ function LoginWithUsername() {
{formik.errors.password}
</small>
)}
<AuthButton type="submit" className="mt-5" disabled={loading}>
{loading ? "در حال ارسال..." : "ورود"}
<AuthButton type="submit" className="mt-5" loading={loading} disabled={loading}>
ورود
</AuthButton>
</form>
<Link href={"/forget-password"}>

View File

@@ -11,6 +11,7 @@ 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 +35,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);
@@ -53,7 +55,8 @@ function Login() {
name="mobile"
maxLength={11}
type="tel"
pattern="[0-9]*"
pattern="[0-9]*"
dir="ltr"
placeholder="09 _ _ _ _ _ _ _ _ _"
className={`border ${
formik.touched.mobile && formik.errors.mobile
@@ -69,8 +72,8 @@ function Login() {
{formik.errors.mobile}
</small>
)}
<AuthButton type="submit" className="mt-5" disabled={loading}>
{loading ? "در حال ارسال..." : "ارسال کد"}
<AuthButton type="submit" className="mt-5" loading={loading} disabled={loading}>
ارسال کد
</AuthButton>
</form>
<Link href={"/login-with-username"}>

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,9 +133,9 @@ function VerifyOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
loading={loading} disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
تایید کد
</AuthButton>
</form>
<Link href={"/login-with-username"}>

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,9 +105,9 @@ function RegisterOtp() {
<AuthButton
type="submit"
className="mt-5"
disabled={loading || formik.values.otp.length !== 6}
loading={loading} disabled={loading || otpExpired || formik.values.otp.length !== 6}
>
{loading ? "در حال ارسال..." : "تایید کد"}
تایید کد
</AuthButton>
</form>
<Link href={"/forget-password"}>

View File

@@ -3,7 +3,10 @@
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -45,17 +48,19 @@ function FullNamePage() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="نام و نام خانوادگی" />
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<AuthHead title="نام و نام خانوادگی" />
<div className="w-full max-w-sm flex flex-col items-center">
<AuthInput
name="name"
type="text"
placeholder="نام"
dir="rtl"
className={`border mt-4 ${
formik.touched.name && formik.errors.name
? "border-red-500 dark:border-red-500"
@@ -66,7 +71,7 @@ function FullNamePage() {
onBlur={formik.handleBlur}
/>
{formik.touched.name && formik.errors.name && (
<small className="text-red-500 mt-2 block">
<small className="text-red-500 mt-2 block text-center">
{formik.errors.name}
</small>
)}
@@ -75,6 +80,7 @@ function FullNamePage() {
name="family"
type="text"
placeholder="نام خانوادگی"
dir="rtl"
className={`border mt-4 ${
formik.touched.family && formik.errors.family
? "border-red-500 dark:border-red-500"
@@ -85,17 +91,20 @@ function FullNamePage() {
onBlur={formik.handleBlur}
/>
{formik.touched.family && formik.errors.family && (
<small className="text-red-500 mt-2 block">
<small className="text-red-500 mt-2 block text-center">
{formik.errors.family}
</small>
)}
</div>
</AuthPageContent>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
<AuthFormFooter>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -10,6 +10,7 @@ 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 +34,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);
@@ -52,6 +54,7 @@ function Register() {
name="mobile"
maxLength={11}
type="text"
dir="ltr"
placeholder="09 _ _ _ _ _ _ _ _ _"
className={`border ${
formik.touched.mobile && formik.errors.mobile
@@ -67,8 +70,8 @@ function Register() {
{formik.errors.mobile}
</small>
)}
<AuthButton type="submit" className="mt-5" disabled={loading}>
{loading ? "در حال ارسال..." : "ارسال کد"}
<AuthButton type="submit" className="mt-5" loading={loading} disabled={loading}>
ارسال کد
</AuthButton>
</form>
<button onClick={() => setModalOpen(true)} className="mb-2">

View File

@@ -2,8 +2,11 @@
"use client";
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import Container from "@/components/elements/Container";
import AuthPasswordInput from "@/components/auth/AuthPasswordInput";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -61,18 +64,19 @@ function PasswordPage() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="کلمه عبور" />
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthInput
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<AuthHead title="کلمه عبور" />
<div className="w-full max-w-sm flex flex-col items-center">
<AuthPasswordInput
name="password"
type="password"
placeholder="کلمه عبور"
className={`border mt-4 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.password && formik.errors.password
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -87,11 +91,11 @@ function PasswordPage() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.confirmPassword && formik.errors.confirmPassword
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -105,13 +109,16 @@ function PasswordPage() {
{formik.errors.confirmPassword}
</small>
)}
</div>
</AuthPageContent>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "تایید کلمه عبور"}
<AuthFormFooter>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
تایید کلمه عبور
</AuthNextButton>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -3,28 +3,25 @@
import AuthHead from "@/components/auth/AuthHead";
import AuthInput from "@/components/auth/AuthInput";
import Container from "@/components/elements/Container";
import React from "react";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
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, USERNAME_MIN_LENGTH } from "@/lib/validation/username";
function UsernamePage() {
const router = useRouter();
const { request, loading } = useAxios();
const [isDuplicate, setIsDuplicate] = useState(false);
const [suggestions, setSuggestions] = useState<string[]>([]);
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
@@ -32,60 +29,100 @@ function UsernamePage() {
initialValues: {
username: "",
},
validationSchema: schema,
validationSchema: usernameFormSchema,
onSubmit: async (values) => {
setIsDuplicate(false);
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?.includes("تکراری")) {
setIsDuplicate(true);
setSuggestions(Array.isArray(data.suggestions) ? data.suggestions : []);
} else if (data?.message) {
formik.setFieldError("username", data.message);
}
}
},
});
const handleUsernameChange = (value: string) => {
setIsDuplicate(false);
setSuggestions([]);
formik.setFieldValue("username", sanitizeUsernameInput(value));
};
const handleSuggestionSelect = (username: string) => {
formik.setFieldValue("username", username);
formik.setFieldTouched("username", true, false);
setIsDuplicate(false);
setSuggestions([]);
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthHead title="نام کاربری" />
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<AuthHead title="نام کاربری" />
<div className="w-full max-w-sm flex flex-col items-center">
<AuthInput
name="username"
type="text"
placeholder="نام کاربری خود را وارد کنید"
dir="auto" // ✅ اجازه ورود همه کاراکترها (فارسی، انگلیسی، ایموجی...)
placeholder="نام کاربری"
dir="ltr"
autoComplete="username"
spellCheck={false}
className={`border ${
formik.touched.username && formik.errors.username
(formik.touched.username && formik.errors.username) || isDuplicate
? "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-red-500 mt-2 block">
{isDuplicate && (
<small className="text-red-500 mt-2 block text-center font-medium">
نام کاربری تکراری است
</small>
)}
{formik.touched.username && formik.errors.username && !isDuplicate && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.username}
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
<UsernameSuggestions
suggestions={suggestions}
onSelect={handleSuggestionSelect}
/>
</div>
</AuthPageContent>
<AuthFormFooter>
<AuthNextButton
type="submit"
loading={loading}
disabled={loading || formik.values.username.length < USERNAME_MIN_LENGTH}
>
ثبت و ادامه
</AuthNextButton>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -2,7 +2,10 @@
"use client";
import AuthHead from "@/components/auth/AuthHead";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -58,14 +61,14 @@ function UsertypePage() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
{/* <AuthHead title="نوع کاربری" /> */}
<AuthHead title="" />
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<AuthHead title="" />
<div className="w-full max-w-sm flex flex-col items-center">
{/* User Type Buttons */}
{/* <AuthNextButton
type="button"
@@ -90,35 +93,36 @@ function UsertypePage() {
کارفرما
</AuthNextButton> */}
{/* Error Message */}
{formik.errors.selectedButton && formik.touched.selectedButton && (
<div className="text-red-500 mt-2 text-sm">
{formik.errors.selectedButton}
</div>
)}
</div>
</AuthPageContent>
{/* Submit Buttons */}
<AuthFormFooter>
<div className="flex gap-3">
<AuthNextButton
type="submit"
onClick={() => setSubmitAction("register")}
className="mt-20 !border-[#0C8002] !text-[#0C8002]"
className="!border-[#0C8002] !text-[#0C8002]"
disabled={loading}
>
{loading ? "در حال ارسال..." : "ادامه ثبت نام"}
ادامه ثبت نام
</AuthNextButton>
<AuthNextButton
type="submit"
onClick={() => setSubmitAction("login")}
className="mt-20 !border-[#FF0000] !text-[#FF0000]"
className="!border-[#FF0000] !text-[#FF0000]"
disabled={loading}
>
{loading ? "در حال ارسال..." : "ورود به برنامه"}
ورود به برنامه
</AuthNextButton>
</div>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -69,17 +72,18 @@ function AuthPage() {
console.log(formik.values);
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center auth-page">
<span className="text-xl font-bold">احراز هویت</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<AuthPageLayout className="auth-page">
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<span className="text-xl font-bold">احراز هویت</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<div className="w-full max-w-sm flex flex-col items-center">
<small className="font-bold mt-10 mb-4"> جهت احراز هویت</small>
<div className="flex w-full gap-2 max-w-[300px]">
<div>
@@ -165,19 +169,16 @@ function AuthPage() {
<small className="font-bold mt-10">
شماره شبا باید به نام خود شخص باشد
</small>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/location")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/location")}
type="button"
>
<span>رد کن</span>
</button>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -1,9 +1,12 @@
"use client";
import React, { useEffect, useState, useCallback } from "react";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import { IProfileData } from "@/types/types";
import Image from "next/image";
@@ -20,7 +23,6 @@ function AvatarPage() {
const { request, loading } = useAxios();
const [userDetail, setUserDetail] = useState<IProfileData | null>(null);
const [avatar, setAvatar] = useState<string | null>(null);
const [originalImage, setOriginalImage] = useState<string | null>(null); // تصویر اصلی (سایز کامل)
const [crop, setCrop] = useState({ x: 0, y: 0 });
const [zoom, setZoom] = useState(1);
const [loadingUpload, setLoadingUpload] = useState(false);
@@ -50,8 +52,7 @@ function AvatarPage() {
const reader = new FileReader();
reader.onload = (e) => {
const imageDataUrl = e.target?.result as string;
setAvatar(imageDataUrl); // برای کراپر
setOriginalImage(imageDataUrl); // تصویر اصلی (سایز کامل)
setAvatar(imageDataUrl);
};
reader.readAsDataURL(file);
}
@@ -105,7 +106,6 @@ function AvatarPage() {
} catch (err) {
console.log("Upload error:", err);
toast.error("خطا در ارسال تصویر!");
} finally {
setLoadingUpload(false);
}
};
@@ -120,28 +120,11 @@ function AvatarPage() {
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
{(loading || loadingUpload) && <p>Loading...</p>}
<AuthPageLayout>
<AuthPageContent>
<span className="text-xl font-bold mb-4">تصویر پروفایل</span>
{!loading && !loadingUpload && (
<>
<span className="text-xl font-bold mb-4">تصویر پروفایل</span>
{/* نمایش تصویر اصلی (سایز کامل) */}
{originalImage && (
<div className="relative w-[300px] h-[400px] bg-gray-200 rounded-2xl mb-4 overflow-hidden">
<Image
src={originalImage}
alt="Original image"
fill
className="object-contain"
/>
</div>
)}
{/* کراپر برای برش تصویر به مربع 1:1 */}
<div className="relative w-[250px] h-[250px] bg-gray-200 rounded-3xl overflow-hidden mb-4">
<div className="relative aspect-square w-[250px] bg-gray-200 rounded-3xl overflow-hidden mb-4">
{avatar ? (
<Cropper
image={avatar.startsWith("data:") ? avatar : IMAGE_BASE_URL + avatar}
@@ -178,31 +161,26 @@ function AvatarPage() {
onChange={selectImage}
/>
<AuthNextButton className="mt-4">
<label className="cursor-pointer w-full h-full" htmlFor="fileInput">
انتخاب/ویرایش تصویر
</label>
</AuthNextButton>
<AuthNextButton className="mt-4">
<label className="cursor-pointer w-full h-full" htmlFor="fileInput">
انتخاب/ویرایش تصویر
</label>
</AuthNextButton>
</AuthPageContent>
<AuthNextButton
onClick={uploadImage}
className="mt-10"
disabled={loadingUpload}
>
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={navigateHandler}
type="button"
>
رد کن
</button>
</>
)}
</div>
</Container>
<AuthFormFooter
onSkip={navigateHandler}
skipDisabled={loadingUpload}
>
<AuthNextButton
onClick={uploadImage}
loading={loadingUpload}
disabled={loadingUpload}
>
ثبت و ادامه
</AuthNextButton>
</AuthFormFooter>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import * as yup from "yup";
@@ -88,8 +91,8 @@ function Colors() {
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthPageLayout>
<AuthPageContent>
<span className="text-xl font-bold">سایز</span>
<div className="mt-5">
<AuthUserDetails />
@@ -163,24 +166,19 @@ function Colors() {
</div>
)}
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/public-relations")}>
<AuthNextButton
type="button"
onClick={handleCheck}
className="mt-10"
loading={loading}
disabled={loading}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/public-relations")}
type="button"
>
<span>رد کن</span>
</button>
</div>
</Container>
</AuthFormFooter>
</AuthPageLayout>
);
}

View File

@@ -8,20 +8,23 @@ import AuthNextButton from "@/components/auth/AuthNextButton";
import Modal from "@/components/elements/Modal";
import Link from "next/link";
import Image from "next/image";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { useUser } from "@/hooks/useUser";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
function Confirm() {
const user = useUser();
const router = useRouter();
const [showModal, setShowModal] = useState<boolean>(false);
const [navigating, setNavigating] = useState(false);
const userType: string | null =
typeof window !== "undefined" ? localStorage.getItem("usertype") : null;
const confirmHandler = () => {
setNavigating(true);
if (userType !== "employer") {
setShowModal(true);
setNavigating(false);
} else {
router.push("/");
}
@@ -36,13 +39,11 @@ function Confirm() {
ورود شما به خانواده مدستاگرام را تبریک می گوییم{" "}
</small>
<div className="flex flex-col justify-center items-center">
<Image
className="rounded-2xl object-cover"
width={280}
height={280}
<ProfileAvatar
src={user?.profile_image}
alt={user?.user_name || "user profile"}
src={`${IMAGE_BASE_URL}${user?.profile_image}`}
unoptimized={true}
size="xl"
rounded="2xl"
/>
<div className="flex flex-col justify-center items-center gap-1 mt-1">
<span>
@@ -117,7 +118,7 @@ function Confirm() {
</div>
</div>
{/* */}
<AuthNextButton onClick={confirmHandler} className="mt-20">
<AuthNextButton onClick={confirmHandler} className="mt-20" loading={navigating} disabled={navigating}>
ثبت و ادامه
</AuthNextButton>
<small className="font-bold mt-10 mb-1 text-center">

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -44,20 +47,22 @@ function CooperationType() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold">نوع همکاری</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<span className="text-xl font-bold">نوع همکاری</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<p className="my-10 text-sm font-bold">
آیا مایل به همکاری خارج از محل سکونت خود هستید؟
</p>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<p className="my-10 text-sm font-bold">
آیا مایل به همکاری خارج از محل سکونت خود هستید؟
</p>
<div className="w-full max-w-sm flex flex-col items-center">
<div className="flex w-full gap-2">
<AuthNextButton
type="button"
@@ -83,26 +88,21 @@ function CooperationType() {
</AuthNextButton>
</div>
{/* Error Message */}
{formik.errors.selectedButton && formik.touched.selectedButton && (
<div className="text-red-500 mt-2 text-sm">
{formik.errors.selectedButton}
</div>
)}
</div>
</AuthPageContent>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
<AuthFormFooter onSkip={() => router.push("/verify/public-relations")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/public-relations")}
type="button"
>
<span>رد کن</span>
</button>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useEffect, useState } from "react";
import useAxios from "@/hooks/useAxios";
import * as yup from "yup";
@@ -89,8 +92,8 @@ function Expertise() {
);
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthPageLayout>
<AuthPageContent>
<span className="text-xl font-bold">تخصص</span>
<AuthUserDetails />
@@ -126,25 +129,19 @@ function Expertise() {
</button>
))}
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/services")}>
<AuthNextButton
type="button"
onClick={handleCheck}
className="mt-20"
loading={loading}
disabled={loading}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/services")}
type="button"
>
<span>رد کن</span>
</button>
</div>
</Container>
</AuthFormFooter>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -50,16 +53,18 @@ function Gender() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold">جنسیت</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<span className="text-xl font-bold">جنسیت</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<div className="w-full max-w-sm flex flex-col items-center">
<div className="flex w-full gap-4">
<AuthNextButton
type="button"
@@ -97,26 +102,21 @@ function Gender() {
</AuthNextButton>
</div>
{/* Error Message */}
{formik.errors.selectedButton && formik.touched.selectedButton && (
<div className="text-red-500 mt-2 text-sm">
{formik.errors.selectedButton}
</div>
)}
</div>
</AuthPageContent>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
<AuthFormFooter onSkip={() => router.push("/verify/sizes")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/sizes")}
type="button"
>
<span>رد کن</span>
</button>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useEffect, useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -118,20 +121,22 @@ function LocationPage() {
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center address-page ">
<span className="text-xl font-bold">لوکیشن</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<AuthPageLayout className="address-page">
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<span className="text-xl font-bold">لوکیشن</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<small className="font-bold mt-10 mb-4">
مشخص کنید در کدام شهر قادر به انجام فعالیت هستید
</small>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<small className="font-bold mt-10 mb-4">
مشخص کنید در کدام شهر قادر به انجام فعالیت هستید
</small>
<div className="w-full max-w-sm flex flex-col items-center">
<SelectBox
className={`mt-4 ${
formik.touched.stateId && formik.errors.stateId
@@ -230,20 +235,16 @@ function LocationPage() {
اطلاعات لوکیشن شما برای همه قابل نمایش باشد
</span>
</label>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
</AuthNextButton>
</div>
</AuthPageContent>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/national-cart")}
type="button"
>
<span>رد کن</span>
</button>
</form>
</div>
</Container>
<AuthFormFooter onSkip={() => router.push("/verify/national-cart")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useRouter } from "next/navigation";
@@ -9,7 +12,7 @@ import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import AuthRules from "@/components/auth/AuthRules";
function NationalCart() {
@@ -69,8 +72,8 @@ function NationalCart() {
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center address-page">
<AuthPageLayout className="address-page">
<AuthPageContent>
<span className="text-xl font-bold">احراز هویت</span>
<div className="mt-5">
<AuthUserDetails />
@@ -146,25 +149,19 @@ function NationalCart() {
</span>
</div>
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/confirm")}>
<AuthNextButton
onClick={uploadImage}
className="mt-20"
loading={loading}
disabled={loading}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/confirm")}
type="button"
>
رد کن
</button>
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
</div>
</Container>
</AuthFormFooter>
</AuthPageLayout>
);
}

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
@@ -46,18 +49,18 @@ function PublicRelations() {
});
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold">روابط عمومی</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<AuthPageLayout>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"
>
<AuthPageContent>
<span className="text-xl font-bold">روابط عمومی</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<div className="w-full max-w-sm flex flex-col items-center">
<small className="font-bold mt-10"> در مورد خودتان چیزی بگویید</small>
<textarea
name="bio"
@@ -85,20 +88,16 @@ function PublicRelations() {
{formik.errors.bio}
</small>
)}
</div>
</AuthPageContent>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
<AuthFormFooter onSkip={() => router.push("/verify/location")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/location")}
type="button"
>
<span>رد کن</span>
</button>
</form>
</div>
</Container>
</AuthFormFooter>
</form>
</AuthPageLayout>
);
}

View File

@@ -5,7 +5,10 @@ import { Service } from "@/types/types";
import ServiceItem from "@/components/main/Services/ServiceItem";
import AddServiceModal from "@/components/main/Services/AddServiceModal";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
@@ -80,8 +83,8 @@ const ServicesPage: React.FC = () => {
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center w-full">
<AuthPageLayout>
<AuthPageContent className="w-full">
<span className="text-xl font-bold">خدمات</span>
<div className="mt-5 w-full max-w-md">
@@ -98,23 +101,6 @@ const ServicesPage: React.FC = () => {
))}
</div>
<div className="flex gap-4 mt-6">
<AuthNextButton
onClick={handleSubmit}
disabled={loading}
className="mb-4 w-32"
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
</AuthNextButton>
<AuthNextButton
onClick={() => setModalOpen(true)}
className="mb-4 !text-[#0066FF] !border-[#0066FF] w-32"
>
افزودن
</AuthNextButton>
</div>
{isModalOpen && (
<AddServiceModal
onClose={() => setModalOpen(false)}
@@ -122,15 +108,28 @@ const ServicesPage: React.FC = () => {
isModalOpen={isModalOpen}
/>
)}
<button
className="text-sm text-gray-500 mt-5"
onClick={Reject}
type="button"
>
<span>رد کن</span>
</button>
</div>
</Container>
</AuthPageContent>
<AuthFormFooter onSkip={Reject}>
<div className="flex gap-4">
<AuthNextButton
onClick={handleSubmit}
loading={loading}
disabled={loading}
className="w-32"
>
ثبت و ادامه
</AuthNextButton>
<AuthNextButton
onClick={() => setModalOpen(true)}
className="!text-[#0066FF] !border-[#0066FF] w-32"
>
افزودن
</AuthNextButton>
</div>
</AuthFormFooter>
</AuthPageLayout>
);
};

View File

@@ -1,7 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useState } from "react";
import useAxios from "@/hooks/useAxios";
import * as yup from "yup";
@@ -73,8 +76,8 @@ function Sizes() {
};
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<AuthPageLayout>
<AuthPageContent>
<span className="text-xl font-bold">سایز</span>
<div className="mt-5">
<AuthUserDetails />
@@ -175,24 +178,19 @@ function Sizes() {
</div>
)}
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/colors")}>
<AuthNextButton
type="button"
onClick={handleCheck}
className="mt-10"
loading={loading}
disabled={loading}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
ثبت و ادامه
</AuthNextButton>
<button
className="text-sm text-gray-500 mt-5"
onClick={() => router.push("/verify/colors")}
type="button"
>
<span>رد کن</span>
</button>
</div>
</Container>
</AuthFormFooter>
</AuthPageLayout>
);
}

View File

@@ -20,7 +20,7 @@ import {
import { Course } from "@/types/types";
import useAxios from "@/hooks/useAxios";
import { Comments } from "@/components/academy/CommentsModal";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { usePathname } from "next/navigation";
import Cookies from "js-cookie";
@@ -670,7 +670,7 @@ useEffect(() => {
alt={item?.user_id?.user_name || "کاربر"}
src={
item?.user_id?.profile_image
? `${IMAGE_BASE_URL}${item.user_id.profile_image}`
? buildStorageUrl(item.user_id.profile_image)
: "/images/default-avatar.png"
}
priority={true}

View File

@@ -1,6 +1,6 @@
"use client";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import React, { useEffect, useState } from "react";
import { Academy, Course } from "@/types/types";
import Container from "@/components/elements/Container";

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

@@ -1,4 +1,4 @@
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import React, { cache } from "react";
import { cookies } from "next/headers";
import { IAdvertisingProfile } from "@/types/types";

View File

@@ -1,4 +1,4 @@
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import React, { cache } from "react";
import { cookies } from "next/headers";
import { IAdvertisingProfile } from "@/types/types";

View File

@@ -502,7 +502,7 @@ select {
filter: invert(1) hue-rotate(180deg) brightness(1.2);
}
/* حذف پس‌زمینه سفید اضافی در برخی مرورگرها */
.custom-audio-player::-webkit-media-controls-panel {
background-color: transparent;
}
.profile-avatar {
aspect-ratio: 1 / 1;
object-fit: cover;
}

View File

@@ -1,7 +1,7 @@
import { Metadata } from "next";
import { fetchPostById } from "@/api/fetchPostById";
import PostFeedView from "@/components/posts/PostFeedView";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { Suspense } from "react";
import PageLoader from "@/components/ui/PageLoader";

View File

@@ -28,7 +28,7 @@ import "leaflet/dist/leaflet.css";
import { useRouter } from "next/navigation";
import useAxios from "@/hooks/useAxios";
import { Academy } from "@/types/types";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
// Zod schema for store settings
const storeSchema = z.object({

View File

@@ -2,7 +2,7 @@
import Container from "@/components/elements/Container";
import RoundedInput from "@/components/elements/RoundedInput";
import { IMAGE_BASE_URL, SOCKET_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, SOCKET_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import PageTitle from "@/components/settings/PageTitle";
import UserDetails from "@/components/settings/UserDetails";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
@@ -115,7 +115,7 @@ function Chats() {
width={56}
height={56}
alt={item?.user_name}
src={IMAGE_BASE_URL + item?.profile_image}
src={buildStorageUrl(item?.profile_image)}
className="h-14 w-14 rounded-full object-cover"
/>
) : (

View File

@@ -9,7 +9,7 @@ import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import AuthRules from "@/components/auth/AuthRules";
function NationalCart() {
@@ -149,9 +149,9 @@ function NationalCart() {
<AuthNextButton
onClick={uploadImage}
className="mt-20"
disabled={loading}
loading={loading} disabled={loading}
>
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
ثبت و ادامه
</AuthNextButton>
<button

View File

@@ -6,8 +6,9 @@ import React, { useState, useEffect } from "react";
import AuthNextButton from "@/components/auth/AuthNextButton";
import Image from "next/image";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { useUser } from "@/hooks/useUser";
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import Cookies from "js-cookie";
@@ -178,17 +179,13 @@ function LicensePage() {
<span className="text-xl mb-9 font-bold">بارگذاری مجوز</span>
<div className="flex flex-col justify-center items-center">
<Image
className="rounded-2xl object-cover"
width={130}
height={130}
<ProfileAvatar
src={user?.profile_image}
alt={user?.user_name || "user profile"}
src={
user?.profile_image
? `${IMAGE_BASE_URL}${user.profile_image}`
: "/images/placeholder.png"
}
unoptimized={true}
size="md"
rounded="2xl"
className="h-[130px] w-[130px]"
fallback="/images/placeholder.png"
/>
<div className="flex flex-col justify-center items-center gap-1 mt-1">
<span>
@@ -286,7 +283,7 @@ function LicensePage() {
</svg>
</div>
</div>
<AuthNextButton onClick={upload} className="mt-20">
<AuthNextButton onClick={upload} className="mt-20" loading={loadingUpload} disabled={loadingUpload}>
ثبت و ادامه
</AuthNextButton>
<small className="font-bold mt-10 mb-1 text-center">

View File

@@ -2,7 +2,7 @@
import AuthNextButton from "@/components/auth/AuthNextButton";
import Container from "@/components/elements/Container";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import { useUser } from "@/hooks/useUser";
import Image from "next/image";
@@ -58,7 +58,6 @@ function AvatarPage() {
"Content-Type": "multipart/form-data",
},
});
setLoadingUpload(false);
router.push("/settings/edit");
} else {
setLoadingUpload(false);
@@ -80,13 +79,9 @@ function AvatarPage() {
return (
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
{loading || loadingUpload ? (
<p>Loading...</p>
) : (
<>
<div className="flex flex-col items-center mb-4">
<span className="text-xl font-bold">تصویر پروفایل</span>
<div className="relative w-[250px] h-[250px] flex items-center justify-center text-white rounded-3xl border border-[#676767] overflow-hidden mt-10">
<div className="flex flex-col items-center mb-4">
<span className="text-xl font-bold">تصویر پروفایل</span>
<div className="relative aspect-square w-[250px] flex items-center justify-center text-white rounded-3xl border border-[#676767] overflow-hidden mt-10">
{avatar ? (
<>
<img
@@ -96,7 +91,7 @@ function AvatarPage() {
: IMAGE_BASE_URL + avatar
}
alt="Avatar"
className="w-full h-full object-cover"
className="w-full h-full aspect-square object-cover"
/>
<button
onClick={removeImage}
@@ -141,15 +136,14 @@ function AvatarPage() {
</label>
</AuthNextButton>
<AuthNextButton
onClick={uploadImage}
className="mt-20"
disabled={loading}
>
ویرایش
</AuthNextButton>
</>
)}
<AuthNextButton
onClick={uploadImage}
className="mt-20"
loading={loadingUpload}
disabled={loadingUpload || loading}
>
ویرایش
</AuthNextButton>
</div>
</Container>
);

View File

@@ -75,8 +75,8 @@ function PublicRelations() {
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
ویرایش
</AuthNextButton>
</form>
</div>

View File

@@ -159,9 +159,9 @@ function Colors() {
type="button"
onClick={handleCheck}
className="mt-10"
disabled={loading}
loading={loading} disabled={loading}
>
{loading ? "در حال ارسال..." : "ویرایش"}
ویرایش
</AuthNextButton>
</div>
</Container>

View File

@@ -89,8 +89,8 @@ function CooperationType() {
</div>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
ویرایش
</AuthNextButton>
</form>
</div>

View File

@@ -134,9 +134,9 @@ function Expertise() {
type="button"
onClick={handleCheck}
className="mt-20"
disabled={loading}
loading={loading} disabled={loading}
>
{loading ? "در حال ارسال..." : "ویرایش"}
ویرایش
</AuthNextButton>
</div>
</Container>

View File

@@ -259,8 +259,8 @@ function LocationPage() {
اطلاعات لوکیشن شما برای همه قابل نمایش باشد
</span>
</label>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
ویرایش
</AuthNextButton>
</form>
</div>

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,11 +64,11 @@ 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 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.password && formik.errors.password
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -83,11 +83,11 @@ function PasswordPage() {
</small>
)}
<AuthInput
<AuthPasswordInput
name="confirmPassword"
type="password"
placeholder="تکرار کلمه عبور"
className={`border mt-4 ${
wrapperClassName="mt-4"
className={`border ${
formik.touched.confirmPassword && formik.errors.confirmPassword
? "border-red-500 dark:border-red-500"
: "border-gray-300"
@@ -102,8 +102,8 @@ function PasswordPage() {
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "تایید کلمه عبور"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
تایید کلمه عبور
</AuthNextButton>
</form>
</div>

View File

@@ -123,8 +123,8 @@ function PublicRelations() {
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
ویرایش
</AuthNextButton>
</form>
</div>

View File

@@ -4,6 +4,7 @@ import React, { useEffect, useState } from "react";
import { Service, User } from "@/types/types";
import ServiceItem from "@/components/main/Services/ServiceItem";
import AddServiceModal from "@/components/main/Services/AddServiceModal";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import Container from "@/components/elements/Container";
import AuthNextButton from "@/components/auth/AuthNextButton";
import useAxios from "@/hooks/useAxios";
@@ -93,6 +94,10 @@ const ServicesPage: React.FC = () => {
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold">خدمات</span>
<div className="mt-5 w-full max-w-md">
<AuthUserDetails />
</div>
<div className="gap-4 min-h-96">
{services.map((service) => (
<ServiceItem
@@ -107,10 +112,10 @@ const ServicesPage: React.FC = () => {
<div className="flex gap-4">
<AuthNextButton
onClick={handleSubmit}
disabled={loading}
loading={loading} disabled={loading}
className="mb-4 w-32"
>
{loading ? "در حال ارسال..." : "ویرایش"}
ویرایش
</AuthNextButton>
<AuthNextButton
onClick={() => setModalOpen(true)}

View File

@@ -87,8 +87,8 @@ function AuthPage() {
<small className="font-bold mt-10">
شماره شبا باید به نام خود شخص باشد
</small>
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
ویرایش
</AuthNextButton>
</form>
</div>

View File

@@ -175,9 +175,9 @@ function Sizes() {
type="button"
onClick={handleCheck}
className="mt-10"
disabled={loading}
loading={loading} disabled={loading}
>
{loading ? "در حال ارسال..." : "ویرایش"}
ویرایش
</AuthNextButton>
</div>
</Container>

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,57 @@ 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, USERNAME_MIN_LENGTH } from "@/lib/validation/username";
function UsernamePage() {
const router = useRouter();
const user = useUser();
const { request, loading } = useAxios();
const [isDuplicate, setIsDuplicate] = useState(false);
const [suggestions, setSuggestions] = useState<string[]>([]);
const formik = useFormik({
initialValues: {
username: user?.user_name || "",
},
validationSchema: schema,
validationSchema: usernameFormSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setIsDuplicate(false);
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?.includes("تکراری")) {
setIsDuplicate(true);
setSuggestions(Array.isArray(data.suggestions) ? data.suggestions : []);
} else if (data?.message) {
formik.setFieldError("username", data.message);
}
}
},
});
// آپدیت مقدار username در صورتی که user.user_name مقدار داشته باشد
useEffect(() => {
if (user?.user_name) {
formik.setValues({ username: user.user_name });
}
}, [user?.user_name]);
const handleUsernameChange = (value: string) => {
setIsDuplicate(false);
setSuggestions([]);
formik.setFieldValue("username", sanitizeUsernameInput(value));
};
const handleSuggestionSelect = (username: string) => {
formik.setFieldValue("username", username);
formik.setFieldTouched("username", true, false);
setIsDuplicate(false);
setSuggestions([]);
};
return (
<Container>
@@ -57,23 +72,43 @@ function UsernamePage() {
<AuthInput
name="username"
type="text"
placeholder="نام کاربری خود را وارد کنید"
placeholder="نام کاربری"
dir="ltr"
autoComplete="username"
spellCheck={false}
className={`border ${
formik.touched.username && formik.errors.username
(formik.touched.username && formik.errors.username) || isDuplicate
? "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-red-500 mt-2 block">
{isDuplicate && (
<small className="text-red-500 mt-2 block text-center font-medium">
نام کاربری تکراری است
</small>
)}
{formik.touched.username && formik.errors.username && !isDuplicate && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.username}
</small>
)}
<AuthNextButton type="submit" className="mt-20" disabled={loading}>
{loading ? "در حال ارسال..." : "ویرایش"}
<UsernameSuggestions
suggestions={suggestions}
onSelect={handleSuggestionSelect}
/>
<AuthNextButton
type="submit"
className="mt-20"
loading={loading} disabled={loading || formik.values.username.length < USERNAME_MIN_LENGTH}
>
ویرایش
</AuthNextButton>
</form>
</div>

View File

@@ -1,4 +1,4 @@
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import React from "react";
import { cookies } from "next/headers";
import { User } from "@/types/types";
@@ -30,7 +30,7 @@ export async function generateMetadata({ params }: IUserProps): Promise<Metadata
const title = `${fullName} - ${username} - ${expertise} - ${city} - مدستاگرام`;
const description = `${expertise} - ${city} - ${bio}`.slice(0, 160);
const profileImage = user.profile_image ? `${IMAGE_BASE_URL}${user.profile_image}` : "/images/logo.png";
const profileImage = user.profile_image ? buildStorageUrl(user.profile_image) : "/images/logo.png";
return {
title, // حفظ تایتل درخواستی شما
@@ -106,7 +106,7 @@ async function UserPage({ params }: IUserProps) {
name: fullName,
alternateName: username,
description: user.bio,
image: user.profile_image ? `${IMAGE_BASE_URL}${user.profile_image}` : undefined,
image: user.profile_image ? buildStorageUrl(user.profile_image) : undefined,
jobTitle: user.expertise,
address: {
"@type": "PostalAddress",

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,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { useState } from "react";
type ImageUploaderProps = {

View File

@@ -1,7 +1,7 @@
import React, { useState, useRef, useCallback, useEffect } from "react";
import Modal from "../elements/Modal";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import RoundedInput from "@/components/elements/RoundedInput";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
@@ -370,7 +370,7 @@ function CommentsModal({
alt={item?.user_id?.user_name || "کاربر"}
src={
item?.user_id?.profile_image
? `${IMAGE_BASE_URL}${item.user_id.profile_image}`
? buildStorageUrl(item.user_id.profile_image)
: "/images/default-avatar.png"
}
priority={true}

View File

@@ -12,7 +12,7 @@ import { Academy, Course } from "@/types/types";
import Image from "next/image";
import MainModelCardActions from "./MainModelCardActions";
import Link from "next/link";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { useUserById } from "@/hooks/getUserById";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";

View File

@@ -1,7 +1,5 @@
"use client"
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { User } from "@/types/types";
import Image from "next/image";
import React, { useState } from "react";
import ModelHeadRowTwo from "../models/ModelPage/ModelHeadRowTwo";
import ModelHeadRowThree from "../models/ModelPage/ModelHeadRowThree";
@@ -108,25 +106,13 @@ function ModelHead({ user, item2 }: ModelHeadProps) {
</div>
</div>
</div>
{profile_image ? (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
alt={user_name}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
/>
) : (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
alt={user_name}
src={`/images/fake-avatar.png`}
unoptimized={true}
/>
)}
<ProfileAvatar
src={profile_image}
alt={user_name}
size="md"
rounded="xl"
className="md:h-[120px] md:w-[120px]"
/>
</div>
<ModelHeadRowTwo
first_name={first_name}

View File

@@ -1,13 +1,42 @@
import React, { ComponentProps } from "react";
type TButton = ComponentProps<"button"> & {};
function AuthButton({ children, style, className, ...rest }: TButton) {
import { cn } from "@/lib/utils";
type TButton = ComponentProps<"button"> & {
loading?: boolean;
loadingText?: string;
};
function AuthButton({
children,
style,
className,
loading = false,
loadingText = "در حال ارسال...",
disabled,
...rest
}: TButton) {
return (
<button
className={`w-full dir-ltr max-w-[290px] p-3 text-center text-white rounded-2xl bg-[#FC8EAC] hover:bg-[#f85e87] font-bold duration-300 ${className}`}
style={{ ...style }}
className={cn(
"w-full dir-ltr max-w-[290px] p-3 text-center text-white rounded-2xl bg-[#FC8EAC] hover:bg-[#f85e87] font-bold duration-300",
loading && "cursor-wait opacity-80",
className
)}
style={style}
disabled={disabled || loading}
{...rest}
>
{children}
{loading ? (
<span className="inline-flex items-center justify-center gap-2">
<span
className="h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
aria-hidden
/>
<span>{loadingText}</span>
</span>
) : (
children
)}
</button>
);
}

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 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

@@ -1,13 +1,42 @@
import React, { ComponentProps } from "react";
type TButton = ComponentProps<"button"> & {};
function AuthNextButton({ children, style, className, ...rest }: TButton) {
import { cn } from "@/lib/utils";
type TButton = ComponentProps<"button"> & {
loading?: boolean;
loadingText?: string;
};
function AuthNextButton({
children,
style,
className,
loading = false,
loadingText = "در حال ارسال...",
disabled,
...rest
}: TButton) {
return (
<button
className={`border dir-ltr p-7 py-2 text-center text-border-secondary-light dark:text-border-secondary-dark border-border-secondary-light dark:border-border-secondary-dark rounded-3xl bg-tertiary-light dark:bg-tertiary-dark font-medium duration-300 hover:opacity-70 text-sm ${className}`}
style={{ ...style }}
className={cn(
"border dir-ltr p-7 py-2 text-center text-border-secondary-light dark:text-border-secondary-dark border-border-secondary-light dark:border-border-secondary-dark rounded-3xl bg-tertiary-light dark:bg-tertiary-dark font-medium duration-300 hover:opacity-70 text-sm",
loading && "cursor-wait opacity-70",
className
)}
style={style}
disabled={disabled || loading}
{...rest}
>
{children}
{loading ? (
<span className="inline-flex items-center justify-center gap-2">
<span
className="h-4 w-4 animate-spin rounded-full border-2 border-current border-t-transparent"
aria-hidden
/>
<span>{loadingText}</span>
</span>
) : (
children
)}
</button>
);
}

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,79 @@
import React from "react";
import Container from "@/components/elements/Container";
import { cn } from "@/lib/utils";
type AuthPageLayoutProps = {
children: React.ReactNode;
className?: string;
};
export function AuthPageContent({
children,
className,
}: {
children: React.ReactNode;
className?: string;
}) {
return (
<div
className={cn(
"flex w-full flex-1 flex-col items-center justify-center",
className
)}
>
{children}
</div>
);
}
type AuthFormFooterProps = {
children: React.ReactNode;
onSkip?: () => void;
skipDisabled?: boolean;
className?: string;
};
export function AuthFormFooter({
children,
onSkip,
skipDisabled,
className,
}: AuthFormFooterProps) {
return (
<div
className={cn(
"mt-auto flex w-full flex-col items-center gap-3 pt-6 pb-[max(2rem,env(safe-area-inset-bottom))]",
className
)}
>
{children}
{onSkip && (
<button
type="button"
onClick={onSkip}
disabled={skipDisabled}
className="text-sm text-gray-500 disabled:opacity-50"
>
رد کن
</button>
)}
</div>
);
}
function AuthPageLayout({ children, className }: AuthPageLayoutProps) {
return (
<Container>
<div
className={cn(
"flex min-h-screen flex-col items-center p-4",
className
)}
>
{children}
</div>
</Container>
);
}
export default AuthPageLayout;

View File

@@ -0,0 +1,55 @@
"use client";
import React, { ComponentProps, useState } from "react";
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 m-0 flex size-[22px] items-center justify-center rounded p-0 hover:opacity-70 transition-opacity"
aria-label={visible ? "مخفی کردن کلمه عبور" : "نمایش کلمه عبور"}
tabIndex={-1}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={visible ? "/images/icons/eye-slash.png" : "/images/icons/eye-open.png"}
alt=""
width={22}
height={22}
className="block size-[22px] object-contain object-center opacity-80"
draggable={false}
/>
</button>
</div>
);
}
export default AuthPasswordInput;

View File

@@ -1,4 +1,5 @@
"use client";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import useAxios from "@/hooks/useAxios";
import { IProfileData } from "@/types/types";
import React, { useEffect, useState } from "react";
@@ -6,7 +7,7 @@ import React, { useEffect, useState } from "react";
function AuthUserDetails() {
const [userDetail, setUserDetail] = useState<IProfileData | null>(null);
const { request } = useAxios();
// Fetch user data
const fetchData = async () => {
try {
const response = await request<{ user: IProfileData }>("GET", "/profile");
@@ -15,17 +16,25 @@ function AuthUserDetails() {
console.log(err);
}
};
useEffect(() => {
fetchData();
}, []);
return (
<>
<div className="mt-5 flex w-full max-w-md flex-col items-center text-center">
<ProfileAvatar
src={userDetail?.profile_image}
alt={userDetail?.user_name || "profile"}
size="md"
rounded="xl"
className="mb-3"
/>
<span className="text-lg font-bold">
{userDetail?.first_name} {userDetail?.last_name}
</span>
<p className="text-sm text-gray-500">{userDetail?.user_name}</p>
</>
</div>
);
}

View File

@@ -0,0 +1,30 @@
"use client";
import React from "react";
interface UsernameSuggestionsProps {
suggestions: string[];
onSelect: (username: string) => void;
}
export default function UsernameSuggestions({
suggestions,
onSelect,
}: UsernameSuggestionsProps) {
if (!suggestions.length) return null;
return (
<div className="flex flex-wrap justify-center gap-2 mt-3 w-full max-w-[290px]">
{suggestions.map((username) => (
<button
key={username}
type="button"
onClick={() => onSelect(username)}
className="px-3 py-1.5 text-sm font-medium rounded-full border border-[#387E65] text-[#387E65] bg-white hover:bg-[#387E65]/10 transition-colors dir-ltr"
>
{username}
</button>
))}
</div>
);
}

View File

@@ -5,7 +5,7 @@ import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import { Pagination } from "swiper/modules";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
interface BillboardImageSliderProps {
images: string[];

View File

@@ -2,7 +2,7 @@ import React, { useState } from "react";
import Modal from "../../elements/Modal";
import Image from "next/image";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import RoundedInput from "@/components/elements/RoundedInput";
import useAxios from "@/hooks/useAxios";
@@ -81,7 +81,7 @@ function BillboardCommentsModal({
width={50}
height={50}
alt={item?.userId?.user_name}
src={`${IMAGE_BASE_URL}${item?.userId?.profile_image}`}
src={buildStorageUrl(item?.userId?.profile_image)}
priority={true}
unoptimized={true}
/>

View File

@@ -1,5 +1,5 @@
import RoundedDiv from "@/components/elements/RoundedDiv";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { IAdvertising } from "@/types/types";
import Image from "next/image";
import Link from "next/link";

View File

@@ -1,5 +1,5 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { useState } from "react";
type ImageUploaderProps = {

View File

@@ -1,7 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import React, { useState } from "react";
import Modal from "react-modal";
import { Swiper, SwiperSlide } from "swiper/react";

View File

@@ -1,5 +1,4 @@
"use client"
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { IAdvertisingProfile } from "@/types/types";
import Image from "next/image";
import React, { useState } from "react";
@@ -40,13 +39,12 @@ function AdProfileHead({ profile, id }: AdProfileHeadProps) {
</div>
</div>
{profile_image ? (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
<ProfileAvatar
src={profile_image}
alt={profile?.vitrine_name || "Name"}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
size="md"
rounded="xl"
className="md:h-[120px] md:w-[120px]"
/>
) : (
<div className="border dark:border-border-primary-dark rounded-3xl p-2">
@@ -92,13 +90,12 @@ function AdProfileHead({ profile, id }: AdProfileHeadProps) {
</div>
</div>
{profile_image ? (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
<ProfileAvatar
src={profile_image}
alt={profile?.vitrine_name || "Name"}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
size="md"
rounded="xl"
className="md:h-[120px] md:w-[120px]"
/>
) : (
<div className="border dark:border-border-primary-dark rounded-3xl p-2">

View File

@@ -5,7 +5,7 @@ import Image from "next/image";
import Link from "next/link";
import { useState } from "react";
import { FiChevronRight } from "react-icons/fi";
import { IMAGE_BASE_URL } from "../main/BaseUrl";
import ProfileAvatar from "../main/ProfileAvatar";
import { useRouter } from "next/navigation";
import ChatHeaderMenu from "./ChatHeaderMenu";
@@ -42,12 +42,11 @@ function ChatHeader({
className="gentle-transition flex min-w-0 flex-1 items-center gap-2 active:opacity-80"
>
{user?.profile_image && !user?.blocked_you ? (
<Image
width={40}
height={40}
alt=""
src={IMAGE_BASE_URL + user.profile_image}
className="h-10 w-10 shrink-0 rounded-full object-cover"
<ProfileAvatar
src={user.profile_image}
alt={user.user_name || ""}
size="sm"
rounded="full"
/>
) : (
<div className="h-10 w-10 shrink-0 rounded-full bg-neutral-200 dark:bg-neutral-700" />

View File

@@ -5,7 +5,7 @@ import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Link from "next/link";
import { User } from "@/types/types";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import ReadReceipt from "./ReadReceipt";
import VoiceMessagePlayer from "./VoiceMessagePlayer";
import FileMessageBubble from "./FileMessageBubble";

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import IOSSpinner from "@/components/ui/IOSSpinner";
@@ -147,7 +147,7 @@ export default function ForwardMessageModal({
>
{u.profile_image ? (
<Image
src={IMAGE_BASE_URL + u.profile_image}
src={buildStorageUrl(u.profile_image)}
width={40}
height={40}
alt=""

View File

@@ -1,6 +1,6 @@
import Image from "next/image";
import { User } from "@/types/types";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { FiCheck, FiCheckCircle } from "react-icons/fi"; // اضافه کردن آیکون‌ها
interface Message {

View File

@@ -32,7 +32,7 @@ import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Trash2, Video, Plus, Upload, FileVideo, Play } from "lucide-react";
import useAxios from "@/hooks/useAxios";
import { BASE_URL, IMAGE_BASE_URL } from "../main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "../main/BaseUrl";
// ==========================================
// تغییر: حذف IAdvertisingCategory و استفاده از نوع جدید

View File

@@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Edit, Trash2, Video, Clock, DollarSign, Highlighter, ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import { IMAGE_BASE_URL } from "../main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "../main/BaseUrl";
import PageLoader from "@/components/ui/PageLoader";
interface CourseListProps {

View File

@@ -5,7 +5,7 @@ import Link from "next/link";
import { useInfiniteQuery } from "@tanstack/react-query";
import { fetchPosts } from "@/api/fetchPosts";
import { Post } from "@/types/types";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import Cookies from "js-cookie";
import { useCallback, useEffect } from "react";
import PageLoader from "@/components/ui/PageLoader";

View File

@@ -1,7 +1,30 @@
// لوکال: 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://app.modstagram.ir";
export const BASE_URL = "https://app.modstagram.ir/api/v1";
export const IMAGE_BASE_URL = "https://app.modstagram.ir/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 {
if (!path) return "";
if (path.startsWith("http://") || path.startsWith("https://")) return path;
let normalized = String(path).replace(/\\/g, "/");
normalized = normalized.replace("/root/modstagram-back/storage", "");
if (normalized.startsWith("/storage/")) {
normalized = normalized.slice("/storage".length);
} else if (normalized.startsWith("storage/")) {
normalized = `/${normalized.slice("storage/".length)}`;
}
if (!normalized.startsWith("/")) normalized = `/${normalized}`;
return `${IMAGE_BASE_URL.replace(/\/$/, "")}${normalized}`;
}

View File

@@ -0,0 +1,56 @@
import Image from "next/image";
import { cn } from "@/lib/utils";
import { buildStorageUrl } from "./BaseUrl";
const sizeMap = {
xs: "h-8 w-8",
sm: "h-10 w-10",
md: "h-[72px] w-[72px]",
lg: "h-[120px] w-[120px]",
xl: "h-[280px] w-[280px]",
};
type ProfileAvatarProps = {
src?: string | null;
alt?: string;
size?: keyof typeof sizeMap;
rounded?: "2xl" | "xl" | "full";
className?: string;
fallback?: string;
};
function ProfileAvatar({
src,
alt = "",
size = "md",
rounded = "2xl",
className,
fallback = "/images/fake-avatar.png",
}: ProfileAvatarProps) {
const roundedClass =
rounded === "full"
? "rounded-full"
: rounded === "xl"
? "rounded-xl"
: "rounded-2xl";
const imageSrc = src ? buildStorageUrl(src) : fallback;
return (
<Image
src={imageSrc}
alt={alt}
width={280}
height={280}
unoptimized
className={cn(
"profile-avatar aspect-square object-cover shrink-0",
sizeMap[size],
roundedClass,
className
)}
/>
);
}
export default ProfileAvatar;

View File

@@ -4,7 +4,7 @@ import AuthNextButton from "@/components/auth/AuthNextButton";
import Modal from "@/components/elements/Modal";
import { Service } from "@/types/types";
import React, { useState } from "react";
import { IMAGE_BASE_URL } from "../BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "../BaseUrl";
import Image from "next/image";
import toast from "react-hot-toast";
import { v4 as uuidv4 } from "uuid"; // ✅ استفاده از uuid

View File

@@ -3,7 +3,7 @@
import { Service } from "@/types/types";
import Image from "next/image";
import React from "react";
import { IMAGE_BASE_URL } from "../BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "../BaseUrl";
interface ServiceItemProps {
service: Service;

View File

@@ -1,7 +1,7 @@
import Image from "next/image";
import React from "react";
import { IMAGE_BASE_URL } from "./BaseUrl";
import Link from "next/link";
import ProfileAvatar from "./ProfileAvatar";
interface IUserInfoProps {
profile_image: string | null | undefined;
@@ -22,50 +22,22 @@ function UserInfo({
is_verified,
noLink,
}: IUserInfoProps) {
const avatar = (
<ProfileAvatar
src={profile_image}
alt={user_name || ""}
size="md"
rounded="2xl"
className="ml-3"
/>
);
return (
<div className="flex items-center text-xs max-sm:text-[10px] md:text-sm font-semibold">
{noLink ? (
profile_image ? (
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] ml-3"
width={75}
height={75}
alt={user_name || ""}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
/>
) : (
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] ml-3"
width={75}
height={75}
alt={user_name || ""}
src={`/images/fake-avatar.png`}
unoptimized={true}
/>
)
avatar
) : (
<Link href={`/users/${user_name}`}>
{profile_image ? (
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] ml-3"
width={75}
height={75}
alt={user_name || ""}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
/>
) : (
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] ml-3"
width={75}
height={75}
alt={user_name || ""}
src={`/images/fake-avatar.png`}
unoptimized={true}
/>
)}
</Link>
<Link href={`/users/${user_name}`}>{avatar}</Link>
)}
<div className="flex flex-col gap-1">
<span className="text-[#387E65]">
@@ -88,14 +60,16 @@ function UserInfo({
alt={"not-verify icon"}
src={`/images/icons/not-verify.svg`}
/>
) :is_verified === "true" ? (
) : is_verified === "true" ? (
<Image
width={22}
height={22}
alt="verify icon"
src="/images/icons/verify2.svg"
/>
): ("")}
) : (
""
)}
</h2>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from "react";
import Modal from "../../elements/Modal";
import Image from "next/image";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import RoundedInput from "@/components/elements/RoundedInput";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
@@ -120,11 +120,11 @@ function CommentsModal({ isOpen, onClose, userId, postId }: CommentsModalProps)
className="mb-4 flex flex-row border-b border-neutral-100 pb-2 dark:border-neutral-800"
>
<Image
className="ml-2 min-h-16 max-h-16 min-w-16 max-w-16 rounded-xl"
className="ml-2 aspect-square min-h-16 max-h-16 min-w-16 max-w-16 rounded-xl object-cover"
width={50}
height={50}
alt={item?.user?.user_name}
src={`${IMAGE_BASE_URL}${item?.user?.profile_image}`}
src={buildStorageUrl(item?.user?.profile_image)}
unoptimized
/>
<div className="text-sm">

View File

@@ -11,7 +11,7 @@ import { Post } from "@/types/types";
import Image from "next/image";
import MainModelCardActions from "./MainModelCardActions";
import Link from "next/link";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { useUserById } from "@/hooks/getUserById";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
@@ -170,13 +170,14 @@ function MainModelCard({ postData }: { postData: Post }) {
<Image
src={
profile_image
? `${IMAGE_BASE_URL}${profile_image}`
? buildStorageUrl(profile_image)
: "/images/fake-avatar.png"
}
alt={user_name}
fill
className="rounded-xl object-cover"
className="rounded-xl object-cover aspect-square"
priority
unoptimized
/>
</div>
<div className="font-semibold flex gap-2 flex-col-reverse ml-2 w-full">

View File

@@ -5,7 +5,7 @@ import { Post } from "@/types/types";
import Image from "next/image";
import MainModelCardActions from "../MainModelCard/MainModelCardActions";
import Link from "next/link";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { useUserById } from "@/hooks/getUserById";
import Slider from "react-slick";
import "slick-carousel/slick/slick.css";
@@ -169,12 +169,12 @@ function MainModelCard({ postData }: { postData: Post }) {
<Image
src={
profile_image
? `${IMAGE_BASE_URL}${profile_image}`
? buildStorageUrl(profile_image)
: "/images/fake-avatar.png"
}
alt={user_name}
fill
className="rounded-xl object-cover"
className="rounded-xl object-cover aspect-square"
priority
/>
</div>

View File

@@ -6,7 +6,7 @@ import { useRouter } from "next/navigation";
import { fetchPosts } from "@/api/fetchPosts";
import { Post, User } from "@/types/types";
import axios from "axios";
import { BASE_URL, IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import Image from "next/image";
import { FaPlay } from "react-icons/fa";
import { ChatListSkeleton } from "@/components/ui/ChatSkeletons";

View File

@@ -1,7 +1,5 @@
"use client"
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { User } from "@/types/types";
import Image from "next/image";
import React, { useState } from "react";
import ModelHeadRowTwo from "./ModelHeadRowTwo";
import ModelHeadRowThree from "./ModelHeadRowThree";
@@ -108,25 +106,13 @@ function ModelHead({ user, item2 }: ModelHeadProps) {
</div>
</div>
</div>
{profile_image ? (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
alt={user_name}
src={`${IMAGE_BASE_URL}${profile_image}`}
unoptimized={true}
/>
) : (
<Image
className="rounded-xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px]"
width={120}
height={120}
alt={user_name}
src={`/images/fake-avatar.png`}
unoptimized={true}
/>
)}
<ProfileAvatar
src={profile_image}
alt={user_name}
size="md"
rounded="xl"
className="md:h-[120px] md:w-[120px]"
/>
</div>
<ModelHeadRowTwo
first_name={first_name}

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import Image from "next/image";
import useAxios from "@/hooks/useAxios";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { FiUserPlus, FiX } from "react-icons/fi";
import toast from "react-hot-toast";
@@ -111,7 +111,7 @@ export default function TagUsersPicker({
>
{u.profile_image ? (
<Image
src={IMAGE_BASE_URL + u.profile_image}
src={buildStorageUrl(u.profile_image)}
alt=""
width={32}
height={32}

View File

@@ -1,7 +1,7 @@
import { IProjectCreator } from "@/types/types";
import React from "react";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import Link from "next/link";
function ProjectCreator({ creator }: { creator: IProjectCreator }) {
@@ -11,13 +11,12 @@ function ProjectCreator({ creator }: { creator: IProjectCreator }) {
className="w-full flex items-center my-3"
>
{creator?.profile_image && (
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px] ml-2"
width={120}
height={120}
<ProfileAvatar
src={creator.profile_image}
alt={creator?.user_name}
src={`${IMAGE_BASE_URL}${creator?.profile_image}`}
unoptimized={true}
size="md"
rounded="2xl"
className="ml-2 md:h-[120px] md:w-[120px]"
/>
)}
<div className="flex flex-col gap-1">

View File

@@ -2,7 +2,7 @@
import Image from "next/image";
import React, { useEffect, useState } from "react";
import { IMAGE_BASE_URL } from "../main/BaseUrl";
import ProfileAvatar from "../main/ProfileAvatar";
import CompleteRegister from "./settings/CompleteRegister";
import Link from "next/link";
import { useUser } from "@/hooks/useUser";
@@ -10,27 +10,23 @@ import { useUser } from "@/hooks/useUser";
function UserDetails() {
const [isRegister, setIsRegister] = useState<string | undefined>("false");
const user = useUser();
useEffect(() => {
setIsRegister(user?.is_Register);
}, [user]);
return (
<div>
<Link
href={"/settings/profile"}
className="flex items-center text-xs md:text-sm font-semibold"
>
<Image
className="rounded-2xl object-cover h-[72px] w-[72px] md:h-[120px] md:w-[120px] ml-3"
width={120}
height={120}
<ProfileAvatar
src={user?.profile_image}
alt={user?.user_name || "user profile"}
src={`${IMAGE_BASE_URL}${user?.profile_image}`}
unoptimized={true}
size="md"
rounded="2xl"
className="ml-3 md:h-[120px] md:w-[120px]"
/>
<div className="flex flex-col gap-1">
<span className="text-[#387E65]">
@@ -43,29 +39,29 @@ function UserDetails() {
<div className="flex items-center gap-1 mt-1">
{user?.user_name}
{isRegister === "verified" ? (
<Image
width={22}
height={22}
alt={"verify icon"}
src={`/images/icons/verify.svg`}
/>
) : isRegister === "pending" ? (
<Image
width={25}
height={25}
alt={"not-verify icon"}
src={`/images/icons/not-verify.svg`}
/>
) : isRegister === "true" ? (
<Image
width={22}
height={22}
alt="verify icon"
src="/images/icons/verify2.svg"
/>
) : (
""
)}
<Image
width={22}
height={22}
alt={"verify icon"}
src={`/images/icons/verify.svg`}
/>
) : isRegister === "pending" ? (
<Image
width={25}
height={25}
alt={"not-verify icon"}
src={`/images/icons/not-verify.svg`}
/>
) : isRegister === "true" ? (
<Image
width={22}
height={22}
alt="verify icon"
src="/images/icons/verify2.svg"
/>
) : (
""
)}
<span className="mr-4">{user?.mobile}</span>
</div>
</div>

View File

@@ -1,7 +1,7 @@
// components/AvatarUploader.tsx
import React from "react";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
interface AvatarUploaderProps {
avatar: string | null;

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

@@ -1,6 +1,6 @@
/*@typescript-eslint/no-explicit-any*/
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import Head from "next/head";
type SEOProps = {
@@ -24,7 +24,7 @@ const SEO: React.FC<SEOProps> = ({ type, data }) => {
case "user":
title = `${data.first_name} ${data.last_name} `;
description = `${data.bio} - ${data.user_name} - ${data.expertise}`;
image = `${IMAGE_BASE_URL}${data.profile_image}`;
image = buildStorageUrl(data.profile_image);
break;
case "billboard":

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);
}

Some files were not shown because too many files have changed in this diff Show More