Compare commits
2 Commits
7af3af4e05
...
378cf86fa6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
378cf86fa6 | ||
|
|
d234c7f859 |
BIN
public/images/icons/eye-open.png
Normal file
BIN
public/images/icons/eye-open.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
BIN
public/images/icons/eye-slash.png
Normal file
BIN
public/images/icons/eye-slash.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 KiB |
@@ -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>
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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"}>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
"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, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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, buildStorageUrl } 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={buildStorageUrl(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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
40
src/app/api/auth/session/route.ts
Normal file
40
src/app/api/auth/session/route.ts
Normal 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 });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -149,9 +149,9 @@ function NationalCart() {
|
||||
<AuthNextButton
|
||||
onClick={uploadImage}
|
||||
className="mt-20"
|
||||
disabled={loading}
|
||||
loading={loading} disabled={loading}
|
||||
>
|
||||
{loading ? "در حال ارسال..." : "ثبت و ادامه"}
|
||||
ثبت و ادامه
|
||||
</AuthNextButton>
|
||||
|
||||
<button
|
||||
|
||||
@@ -6,6 +6,7 @@ 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, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -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
|
||||
? buildStorageUrl(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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -159,9 +159,9 @@ function Colors() {
|
||||
type="button"
|
||||
onClick={handleCheck}
|
||||
className="mt-10"
|
||||
disabled={loading}
|
||||
loading={loading} disabled={loading}
|
||||
>
|
||||
{loading ? "در حال ارسال..." : "ویرایش"}
|
||||
ویرایش
|
||||
</AuthNextButton>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -134,9 +134,9 @@ function Expertise() {
|
||||
type="button"
|
||||
onClick={handleCheck}
|
||||
className="mt-20"
|
||||
disabled={loading}
|
||||
loading={loading} disabled={loading}
|
||||
>
|
||||
{loading ? "در حال ارسال..." : "ویرایش"}
|
||||
ویرایش
|
||||
</AuthNextButton>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -175,9 +175,9 @@ function Sizes() {
|
||||
type="button"
|
||||
onClick={handleCheck}
|
||||
className="mt-10"
|
||||
disabled={loading}
|
||||
loading={loading} disabled={loading}
|
||||
>
|
||||
{loading ? "در حال ارسال..." : "ویرایش"}
|
||||
ویرایش
|
||||
</AuthNextButton>
|
||||
</div>
|
||||
</Container>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client"
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
82
src/components/auth/AuthOtpInput.tsx
Normal file
82
src/components/auth/AuthOtpInput.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
87
src/components/auth/AuthOtpStatus.tsx
Normal file
87
src/components/auth/AuthOtpStatus.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
79
src/components/auth/AuthPageLayout.tsx
Normal file
79
src/components/auth/AuthPageLayout.tsx
Normal 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;
|
||||
55
src/components/auth/AuthPasswordInput.tsx
Normal file
55
src/components/auth/AuthPasswordInput.tsx
Normal 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;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
30
src/components/auth/UsernameSuggestions.tsx
Normal file
30
src/components/auth/UsernameSuggestions.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
"use client"
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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={buildStorageUrl(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">
|
||||
|
||||
@@ -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, buildStorageUrl } 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={buildStorageUrl(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" />
|
||||
|
||||
@@ -1,10 +1,15 @@
|
||||
// لوکال: NEXT_PUBLIC_BASE_URL=http://localhost:3002/api/v1
|
||||
// export const SOCKET_URL = "http://localhost:8000";
|
||||
// export const BASE_URL = "http://localhost:8000/api/v1";
|
||||
// export const IMAGE_BASE_URL = "http://localhost:8000/storage";
|
||||
|
||||
export const SOCKET_URL = "https://api.modstagram.com";
|
||||
export const BASE_URL = "https://api.modstagram.com/api/v1";
|
||||
export const IMAGE_BASE_URL = "https://api.modstagram.com/storage";
|
||||
/** app.modstagram.ir — SSL معتبر برای fetch سمت سرور Next.js */
|
||||
export const SOCKET_URL =
|
||||
process.env.NEXT_PUBLIC_SOCKET_URL ?? "https://app.modstagram.ir";
|
||||
export const BASE_URL =
|
||||
process.env.NEXT_PUBLIC_BASE_URL ?? "https://app.modstagram.ir/api/v1";
|
||||
export const IMAGE_BASE_URL =
|
||||
process.env.NEXT_PUBLIC_IMAGE_BASE_URL ?? "https://app.modstagram.ir/storage";
|
||||
|
||||
/** ساخت URL کامل فایل storage — مسیرهای DB را یکسان میکند */
|
||||
export function buildStorageUrl(path: string | null | undefined): string {
|
||||
|
||||
56
src/components/main/ProfileAvatar.tsx
Normal file
56
src/components/main/ProfileAvatar.tsx
Normal 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;
|
||||
@@ -1,7 +1,7 @@
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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={buildStorageUrl(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>
|
||||
|
||||
@@ -120,7 +120,7 @@ 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}
|
||||
|
||||
@@ -175,7 +175,7 @@ function MainModelCard({ postData }: { postData: Post }) {
|
||||
}
|
||||
alt={user_name}
|
||||
fill
|
||||
className="rounded-xl object-cover"
|
||||
className="rounded-xl object-cover aspect-square"
|
||||
priority
|
||||
unoptimized
|
||||
/>
|
||||
|
||||
@@ -174,7 +174,7 @@ function MainModelCard({ postData }: { postData: Post }) {
|
||||
}
|
||||
alt={user_name}
|
||||
fill
|
||||
className="rounded-xl object-cover"
|
||||
className="rounded-xl object-cover aspect-square"
|
||||
priority
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
"use client"
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { IProjectCreator } from "@/types/types";
|
||||
import React from "react";
|
||||
import Image from "next/image";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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">
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } 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={buildStorageUrl(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>
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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
23
src/lib/auth/otpTimer.ts
Normal 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
70
src/lib/auth/session.ts
Normal 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());
|
||||
}
|
||||
21
src/lib/validation/username.ts
Normal file
21
src/lib/validation/username.ts
Normal 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);
|
||||
}
|
||||
20
src/lib/validation/usernameSchema.ts
Normal file
20
src/lib/validation/usernameSchema.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import * as yup from "yup";
|
||||
import {
|
||||
USERNAME_MAX_LENGTH,
|
||||
USERNAME_MIN_LENGTH,
|
||||
USERNAME_REGEX,
|
||||
USERNAME_START_MESSAGE,
|
||||
USERNAME_VALIDATION_MESSAGE,
|
||||
} from "./username";
|
||||
|
||||
export const usernameSchema = yup
|
||||
.string()
|
||||
.required("نام کاربری الزامی است")
|
||||
.min(USERNAME_MIN_LENGTH, `نام کاربری باید حداقل ${USERNAME_MIN_LENGTH} کاراکتر باشد`)
|
||||
.max(USERNAME_MAX_LENGTH, `نام کاربری نمیتواند بیشتر از ${USERNAME_MAX_LENGTH} کاراکتر باشد`)
|
||||
.matches(/^[a-zA-Z0-9]/, USERNAME_START_MESSAGE)
|
||||
.matches(USERNAME_REGEX, USERNAME_VALIDATION_MESSAGE);
|
||||
|
||||
export const usernameFormSchema = yup.object({
|
||||
username: usernameSchema,
|
||||
});
|
||||
@@ -1,17 +1,26 @@
|
||||
import { NextRequest, NextResponse } from 'next/server';
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export function middleware(req: NextRequest) {
|
||||
const token = req.cookies.get('token'); // دریافت توکن از کوکی
|
||||
const isPrefetch =
|
||||
req.headers.get("next-router-prefetch") === "1" ||
|
||||
req.headers.get("purpose") === "prefetch";
|
||||
|
||||
// بررسی اینکه مسیر داخل settings هست
|
||||
if (req.nextUrl.pathname.startsWith('/settings') && !token) {
|
||||
return NextResponse.redirect(new URL('/login', req.url)); // انتقال به صفحه لاگین
|
||||
// prefetch بدون کوکی نباید redirect به /login کش شود
|
||||
if (isPrefetch) {
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
return NextResponse.next(); // ادامه پردازش عادی
|
||||
const token = req.cookies.get("token")?.value?.trim();
|
||||
|
||||
if (req.nextUrl.pathname.startsWith("/settings") && !token) {
|
||||
const loginUrl = new URL("/login", req.url);
|
||||
loginUrl.searchParams.set("redirect", req.nextUrl.pathname);
|
||||
return NextResponse.redirect(loginUrl);
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
}
|
||||
|
||||
// تعیین مسیرهایی که این middleware روی آنها اعمال میشود
|
||||
export const config = {
|
||||
matcher: ['/settings/:path*'], // فقط روی مسیرهای settings اعمال شود
|
||||
matcher: ["/settings/:path*"],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user