This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
import AuthButton from "@/components/auth/AuthButton";
|
||||
import AuthHead from "@/components/auth/AuthHead";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import AuthOtpInput from "@/components/auth/AuthOtpInput";
|
||||
@@ -22,7 +21,6 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
function ForgetPasswordOtp() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [otpExpired, setOtpExpired] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
@@ -139,7 +137,6 @@ function ForgetPasswordOtp() {
|
||||
{t("auth.editMobile")}
|
||||
</small>
|
||||
</Link>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -6,7 +6,6 @@ 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";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
@@ -27,7 +26,6 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
function LoginWithUsername() {
|
||||
const { t, i18n } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
const [authError, setAuthError] = useState(false);
|
||||
const [errorMessage, setErrorMessage] = useState("");
|
||||
@@ -176,7 +174,6 @@ function LoginWithUsername() {
|
||||
<span className="text-[#0033EA]">{t("auth.signUp")}</span>
|
||||
</small>
|
||||
</Link>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -4,9 +4,9 @@ import AuthButton from "@/components/auth/AuthButton";
|
||||
import AuthHead from "@/components/auth/AuthHead";
|
||||
import AuthInput from "@/components/auth/AuthInput";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import AuthLanguageToggle from "@/components/auth/AuthLanguageToggle";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
@@ -22,7 +22,6 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
function Login() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const { request, loading } = useAxios();
|
||||
useAuthSessionRedirect();
|
||||
const redirectPath = getSafeRedirectPath();
|
||||
@@ -62,7 +61,8 @@ function Login() {
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container>
|
||||
<div className="p-4 flex flex-col items-center h-screen justify-center">
|
||||
<div className="relative p-4 flex flex-col items-center h-screen justify-center">
|
||||
<AuthLanguageToggle className="absolute top-6 left-1/2 -translate-x-1/2" />
|
||||
<AuthHead title={t("auth.login")} />
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
@@ -99,12 +99,11 @@ function Login() {
|
||||
{t("auth.loginWithUsername")}
|
||||
</small>
|
||||
</Link>
|
||||
<button onClick={() => setModalOpen(true)} className="mb-2">
|
||||
<Link href="/privacy" className="mb-2">
|
||||
<small className="text-[#FF5C00] font-bold text-xs mt-8 block">
|
||||
{t("auth.rules")}
|
||||
</small>
|
||||
</button>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
import AuthButton from "@/components/auth/AuthButton";
|
||||
import AuthHead from "@/components/auth/AuthHead";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import AuthOtpInput from "@/components/auth/AuthOtpInput";
|
||||
@@ -24,7 +23,6 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
function VerifyOtp() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [otpExpired, setOtpExpired] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
@@ -145,7 +143,6 @@ function VerifyOtp() {
|
||||
{t("auth.editMobile")}
|
||||
</small>
|
||||
</Link>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
import AuthButton from "@/components/auth/AuthButton";
|
||||
import AuthHead from "@/components/auth/AuthHead";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import AuthOtpInput from "@/components/auth/AuthOtpInput";
|
||||
@@ -22,7 +21,6 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
function RegisterOtp() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const [otpExpired, setOtpExpired] = useState(false);
|
||||
const [resendLoading, setResendLoading] = useState(false);
|
||||
const [isSuccess, setIsSuccess] = useState(false);
|
||||
@@ -132,7 +130,6 @@ function RegisterOtp() {
|
||||
{t("auth.editMobile")}
|
||||
</small>
|
||||
</Link>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -7,7 +7,7 @@ import AuthPageLayout, {
|
||||
AuthFormFooter,
|
||||
AuthPageContent,
|
||||
} from "@/components/auth/AuthPageLayout";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
@@ -17,6 +17,15 @@ import toast from "react-hot-toast";
|
||||
import { getSafeRedirectPath } from "@/lib/auth/postLogin";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import Image from "next/image";
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
|
||||
function splitInstagramFullName(fullName: string): { name: string; family: string } {
|
||||
const trimmed = fullName.trim();
|
||||
if (!trimmed) return { name: "", family: "" };
|
||||
const [first, ...rest] = trimmed.split(/\s+/);
|
||||
return { name: first, family: rest.join(" ") };
|
||||
}
|
||||
|
||||
function FullNamePage() {
|
||||
const { t } = useTranslation("common");
|
||||
@@ -26,6 +35,25 @@ function FullNamePage() {
|
||||
const [choiceLoading, setChoiceLoading] = useState<"app" | "continue" | null>(
|
||||
null
|
||||
);
|
||||
const [avatar, setAvatar] = useState<string | null>(null);
|
||||
|
||||
const isInstagramProvider =
|
||||
typeof window !== "undefined" &&
|
||||
localStorage.getItem("auth_provider") === "instagram";
|
||||
const instagramFullName =
|
||||
isInstagramProvider ? localStorage.getItem("instagram_full_name") || "" : "";
|
||||
const instagramSuggested = useMemo(
|
||||
() => splitInstagramFullName(instagramFullName),
|
||||
[instagramFullName]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInstagramProvider) return;
|
||||
request<{ user: { profile_image?: string | null } }>("GET", "/profile")
|
||||
.then((res) => setAvatar(res?.user?.profile_image || null))
|
||||
.catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const schema = useMemo(
|
||||
() =>
|
||||
@@ -40,36 +68,6 @@ function FullNamePage() {
|
||||
[t]
|
||||
);
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
family: "",
|
||||
name: "",
|
||||
},
|
||||
validationSchema: schema,
|
||||
onSubmit: async (values, { setSubmitting }) => {
|
||||
try {
|
||||
await request("POST", "/register/fullname", {
|
||||
first_name: values.name.trim(),
|
||||
last_name: values.family.trim() || undefined,
|
||||
});
|
||||
|
||||
localStorage.setItem("first_name", values.name.trim());
|
||||
if (values.family.trim()) {
|
||||
localStorage.setItem("last_name", values.family.trim());
|
||||
} else {
|
||||
localStorage.removeItem("last_name");
|
||||
}
|
||||
|
||||
setNameSaved(true);
|
||||
toast.success(t("auth.nameSavedSuccess"));
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.message || t("auth.saveNameFailed"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const finishRegistration = async (action: "app" | "continue") => {
|
||||
setChoiceLoading(action);
|
||||
try {
|
||||
@@ -92,6 +90,43 @@ function FullNamePage() {
|
||||
}
|
||||
};
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues: {
|
||||
family: instagramSuggested.family,
|
||||
name: instagramSuggested.name,
|
||||
},
|
||||
validationSchema: schema,
|
||||
onSubmit: async (values, { setSubmitting }) => {
|
||||
try {
|
||||
await request("POST", "/register/fullname", {
|
||||
first_name: values.name.trim(),
|
||||
last_name: values.family.trim() || undefined,
|
||||
});
|
||||
|
||||
localStorage.setItem("first_name", values.name.trim());
|
||||
if (values.family.trim()) {
|
||||
localStorage.setItem("last_name", values.family.trim());
|
||||
} else {
|
||||
localStorage.removeItem("last_name");
|
||||
}
|
||||
|
||||
setNameSaved(true);
|
||||
toast.success(t("auth.nameSavedSuccess"));
|
||||
|
||||
// Instagram signups flow straight into the expertise/onboarding wizard —
|
||||
// no manual "enter app or continue" choice, matching the phone/Google flow's
|
||||
// choice screen being skipped only for this provider.
|
||||
if (isInstagramProvider) {
|
||||
await finishRegistration("continue");
|
||||
}
|
||||
} catch (err: any) {
|
||||
toast.error(err?.response?.data?.message || t("auth.saveNameFailed"));
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<AuthPageLayout>
|
||||
@@ -102,6 +137,16 @@ function FullNamePage() {
|
||||
<AuthPageContent>
|
||||
<AuthHead title={t("auth.fullNameTitle")} />
|
||||
<div className="w-full max-w-sm flex flex-col items-center">
|
||||
{isInstagramProvider && avatar && (
|
||||
<Image
|
||||
src={avatar.startsWith("data:") ? avatar : IMAGE_BASE_URL + avatar}
|
||||
alt=""
|
||||
width={72}
|
||||
height={72}
|
||||
unoptimized
|
||||
className="mb-2 h-[72px] w-[72px] rounded-full object-cover"
|
||||
/>
|
||||
)}
|
||||
<AuthInput
|
||||
name="name"
|
||||
type="text"
|
||||
@@ -117,10 +162,18 @@ function FullNamePage() {
|
||||
onChange={formik.handleChange}
|
||||
onBlur={formik.handleBlur}
|
||||
/>
|
||||
{formik.touched.name && formik.errors.name && (
|
||||
{formik.touched.name && formik.errors.name ? (
|
||||
<small className="text-red-500 mt-2 block text-center">
|
||||
{formik.errors.name}
|
||||
</small>
|
||||
) : (
|
||||
isInstagramProvider &&
|
||||
instagramSuggested.name &&
|
||||
formik.values.name === instagramSuggested.name && (
|
||||
<small className="text-gray-400 mt-2 block text-center text-xs">
|
||||
{t("auth.nameSuggestedFromInstagram")}
|
||||
</small>
|
||||
)
|
||||
)}
|
||||
|
||||
<AuthInput
|
||||
@@ -146,7 +199,9 @@ function FullNamePage() {
|
||||
|
||||
{nameSaved && (
|
||||
<p className="text-sm text-green-600 text-center mt-4">
|
||||
{t("auth.nameSavedChoose")}
|
||||
{isInstagramProvider
|
||||
? t("auth.instagramContinuingToProfile")
|
||||
: t("auth.nameSavedChoose")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -161,7 +216,7 @@ function FullNamePage() {
|
||||
>
|
||||
{t("auth.saveAndContinue")}
|
||||
</AuthNextButton>
|
||||
) : (
|
||||
) : !isInstagramProvider ? (
|
||||
<div className="flex flex-col sm:flex-row gap-3 w-full max-w-sm justify-center items-center">
|
||||
<AuthNextButton
|
||||
type="button"
|
||||
@@ -182,7 +237,7 @@ function FullNamePage() {
|
||||
{t("auth.enterApp")}
|
||||
</AuthNextButton>
|
||||
</div>
|
||||
)}
|
||||
) : null}
|
||||
</AuthFormFooter>
|
||||
</form>
|
||||
</AuthPageLayout>
|
||||
|
||||
@@ -4,8 +4,8 @@ import AuthButton from "@/components/auth/AuthButton";
|
||||
import AuthHead from "@/components/auth/AuthHead";
|
||||
import AuthInput from "@/components/auth/AuthInput";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import React, { useMemo, useState } from "react";
|
||||
import AuthLanguageToggle from "@/components/auth/AuthLanguageToggle";
|
||||
import React, { useMemo } from "react";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useFormik } from "formik";
|
||||
import * as yup from "yup";
|
||||
@@ -26,7 +26,6 @@ function normalizeMobileInput(value: string): string {
|
||||
function Register() {
|
||||
const { t, i18n } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [isModalOpen, setModalOpen] = useState(false);
|
||||
const { request, loading } = useAxios();
|
||||
|
||||
const schema = useMemo(
|
||||
@@ -86,7 +85,8 @@ function Register() {
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container>
|
||||
<div className="p-4 flex flex-col items-center h-screen justify-center">
|
||||
<div className="relative p-4 flex flex-col items-center h-screen justify-center">
|
||||
<AuthLanguageToggle className="absolute top-6 left-1/2 -translate-x-1/2" />
|
||||
<AuthHead title={t("auth.register")} />
|
||||
<form
|
||||
onSubmit={formik.handleSubmit}
|
||||
@@ -134,12 +134,11 @@ function Register() {
|
||||
<span className="text-[#0033EA]">{t("auth.login")}</span>
|
||||
</small>
|
||||
</Link>
|
||||
<button type="button" onClick={() => setModalOpen(true)} className="mb-2">
|
||||
<Link href="/privacy" className="mb-2">
|
||||
<small className="text-[#FF5C00] font-bold text-xs mt-8 block">
|
||||
{t("auth.rules")}
|
||||
</small>
|
||||
</button>
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</Link>
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
|
||||
@@ -13,7 +13,7 @@ import toast from "react-hot-toast";
|
||||
import AuthUserDetails from "@/components/auth/AuthUserDetails";
|
||||
import Image from "next/image";
|
||||
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
|
||||
import AuthRules from "@/components/auth/AuthRules";
|
||||
import Link from "next/link";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
|
||||
@@ -24,7 +24,6 @@ function NationalCart() {
|
||||
|
||||
const [nationalCartImg, setNationalCartImg] = useState<string | null>(null);
|
||||
const [isCheckedOne, setIsCheckedOne] = useState(false);
|
||||
const [isModalOpen, setModalOpen] = useState<boolean>(false);
|
||||
|
||||
const toggleCheckBox = () => setIsCheckedOne(!isCheckedOne);
|
||||
|
||||
@@ -144,15 +143,14 @@ function NationalCart() {
|
||||
type="checkbox"
|
||||
onChange={toggleCheckBox}
|
||||
/>
|
||||
<span
|
||||
onClick={() => setModalOpen(true)}
|
||||
<Link
|
||||
href="/privacy"
|
||||
target="_blank"
|
||||
className="text-xs font-bold cursor-pointer"
|
||||
>
|
||||
{t("auth.acceptRulesLabel")}
|
||||
</span>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<AuthRules isModalOpen={isModalOpen} setModalOpen={setModalOpen} />
|
||||
</AuthPageContent>
|
||||
|
||||
<AuthFormFooter onSkip={() => router.push("/verify/confirm")}>
|
||||
|
||||
@@ -19,7 +19,7 @@ function AboutUs() {
|
||||
</div>
|
||||
<div className="flex flex-col w-full mt-6 gap-1">
|
||||
<span>{t("aboutPage.phoneLabel")}</span>
|
||||
<span>09128893712</span>
|
||||
<span>02167586</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full flex justify-center items-center">
|
||||
|
||||
@@ -16,6 +16,7 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
type InstagramCallbackResponse = IVerifyOtp & {
|
||||
instagram_username?: string | null;
|
||||
instagram_full_name?: string | null;
|
||||
instagram_bio?: string | null;
|
||||
instagram_profile_picture_url?: string | null;
|
||||
};
|
||||
@@ -92,6 +93,9 @@ export default function InstagramCallbackPage() {
|
||||
if (raw?.instagram_username) {
|
||||
localStorage.setItem("instagram_username", raw.instagram_username);
|
||||
}
|
||||
if (raw?.instagram_full_name) {
|
||||
localStorage.setItem("instagram_full_name", raw.instagram_full_name);
|
||||
}
|
||||
if (raw?.instagram_bio) {
|
||||
localStorage.setItem("instagram_bio", raw.instagram_bio);
|
||||
}
|
||||
|
||||
40
src/app/instagram-data-deletion/page.tsx
Normal file
40
src/app/instagram-data-deletion/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthLanguageToggle from "@/components/auth/AuthLanguageToggle";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
export default function InstagramDataDeletionStatusPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const searchParams = useSearchParams();
|
||||
const code = searchParams.get("code");
|
||||
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container>
|
||||
<div className="mx-auto flex max-w-2xl flex-col px-4 py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold">
|
||||
{t("settings.edit.instagramImport.dataDeletionStatusTitle")}
|
||||
</h1>
|
||||
<AuthLanguageToggle />
|
||||
</div>
|
||||
|
||||
<p className="text-sm leading-7 text-neutral-600 dark:text-neutral-300">
|
||||
{code
|
||||
? t("settings.edit.instagramImport.dataDeletionStatusDone")
|
||||
: t("settings.edit.instagramImport.dataDeletionInstructions")}
|
||||
</p>
|
||||
|
||||
{code && (
|
||||
<p className="mt-4 text-xs text-neutral-400" dir="ltr">
|
||||
{t("settings.edit.instagramImport.dataDeletionStatusCode")}: {code}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
);
|
||||
}
|
||||
@@ -7,12 +7,12 @@ import RoundedDiv from "@/components/elements/RoundedDiv";
|
||||
import { selectionCardClass } from "@/lib/ui/buttonStyles";
|
||||
import { BASE_URL } from "@/components/main/BaseUrl";
|
||||
import UserInfo from "@/components/main/UserInfo";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import PageTitle from "@/components/settings/PageTitle";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { IOfferType, User } from "@/types/types";
|
||||
import axios from "axios";
|
||||
import { useFormik } from "formik";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
@@ -209,27 +209,10 @@ function TicketChat({ params }: ITicketChatProps) {
|
||||
last_name={userDetail?.last_name}
|
||||
user_name={userDetail?.user_name}
|
||||
noLink
|
||||
trailingContent={
|
||||
<ScoreRateStats score={userDetail?.user_score} rate={userDetail?.rate} />
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<span>{userDetail?.user_score || "0"}</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt=""
|
||||
src={`/images/icons/medal-star.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span>{userDetail?.rate || "0"}</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt=""
|
||||
src={`/images/icons/star1.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
|
||||
45
src/app/privacy/page.tsx
Normal file
45
src/app/privacy/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Container from "@/components/elements/Container";
|
||||
import AuthLanguageToggle from "@/components/auth/AuthLanguageToggle";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
|
||||
export default function PrivacyPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const [rules, setRules] = useState<string>("");
|
||||
const { request, loading, error } = useAxios();
|
||||
|
||||
useEffect(() => {
|
||||
request<{ value: string }>("GET", "/settings/terms-and-conditions")
|
||||
.then((data) => setRules(data?.value || ""))
|
||||
.catch(() => {});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container>
|
||||
<div className="mx-auto flex max-w-2xl flex-col px-4 py-6">
|
||||
<div className="mb-6 flex items-center justify-between">
|
||||
<h1 className="text-lg font-bold">{t("auth.rules")}</h1>
|
||||
<AuthLanguageToggle />
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<p>{t("auth.rulesLoading")}</p>
|
||||
) : error ? (
|
||||
<p>{t("auth.rulesError")}</p>
|
||||
) : (
|
||||
<div
|
||||
className="text-sm leading-7"
|
||||
dangerouslySetInnerHTML={{ __html: rules }}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
);
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import PageTitle from "@/components/settings/PageTitle";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import InstagramSignInButton from "@/components/auth/InstagramSignInButton";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import Link from "next/link";
|
||||
import React, { useCallback, useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -15,6 +16,7 @@ type InstagramStatus = {
|
||||
instagram_username: string | null;
|
||||
instagram_account_type: string | null;
|
||||
connected_at: string | null;
|
||||
instagram_sync_prefs?: { posts: boolean; auto_update: boolean };
|
||||
};
|
||||
|
||||
export default function InstagramImportSettingsPage() {
|
||||
@@ -56,6 +58,10 @@ export default function InstagramImportSettingsPage() {
|
||||
};
|
||||
}, []);
|
||||
|
||||
const [showModeChoice, setShowModeChoice] = useState(false);
|
||||
const [importingAuto, setImportingAuto] = useState(false);
|
||||
const [togglingAutoOff, setTogglingAutoOff] = useState(false);
|
||||
|
||||
const handleDisconnect = async () => {
|
||||
setDisconnecting(true);
|
||||
try {
|
||||
@@ -69,6 +75,35 @@ export default function InstagramImportSettingsPage() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleAutomaticImport = async () => {
|
||||
setImportingAuto(true);
|
||||
try {
|
||||
await request("POST", "/auth/instagram/import", { mode: "automatic" });
|
||||
toast.success(t("settings.edit.instagramImport.automaticStartSuccess"));
|
||||
setShowModeChoice(false);
|
||||
await loadStatus();
|
||||
} catch {
|
||||
toast.error(t("settings.edit.instagramImport.importError"));
|
||||
} finally {
|
||||
setImportingAuto(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTurnOffAutoUpdate = async () => {
|
||||
setTogglingAutoOff(true);
|
||||
try {
|
||||
await request("POST", "/auth/instagram/import/auto-update", {});
|
||||
toast.success(t("settings.edit.instagramImport.autoUpdateOffSuccess"));
|
||||
await loadStatus();
|
||||
} catch {
|
||||
toast.error(t("settings.edit.instagramImport.importError"));
|
||||
} finally {
|
||||
setTogglingAutoOff(false);
|
||||
}
|
||||
};
|
||||
|
||||
const autoUpdateOn = Boolean(status?.instagram_sync_prefs?.auto_update);
|
||||
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container>
|
||||
@@ -86,6 +121,71 @@ export default function InstagramImportSettingsPage() {
|
||||
@{status.instagram_username}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{autoUpdateOn && (
|
||||
<p className="mt-4 text-sm font-semibold text-[#0C8002]">
|
||||
{t("settings.edit.instagramImport.autoUpdateOnStatus")}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!showModeChoice ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowModeChoice(true)}
|
||||
className="mt-6 rounded-2xl border-2 border-[#0095f6] px-4 py-2 text-sm font-semibold text-[#0095f6]"
|
||||
>
|
||||
{t("settings.edit.instagramImport.updateButton")}
|
||||
</button>
|
||||
) : (
|
||||
<div className="mt-6 flex w-full flex-col gap-3">
|
||||
<p className="text-sm font-semibold">
|
||||
{t("settings.edit.instagramImport.chooseModeTitle")}
|
||||
</p>
|
||||
<Link
|
||||
href="/settings/edit/instagram-import/select"
|
||||
className="rounded-2xl border-2 border-gray-200 px-4 py-3 text-sm dark:border-gray-700"
|
||||
>
|
||||
<span className="block font-semibold">
|
||||
{t("settings.edit.instagramImport.manualMode")}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-neutral-500">
|
||||
{t("settings.edit.instagramImport.manualModeHint")}
|
||||
</span>
|
||||
</Link>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleAutomaticImport}
|
||||
disabled={importingAuto}
|
||||
className="rounded-2xl border-2 border-gray-200 px-4 py-3 text-right text-sm disabled:opacity-60 dark:border-gray-700"
|
||||
>
|
||||
<span className="block font-semibold">
|
||||
{t("settings.edit.instagramImport.automaticMode")}
|
||||
</span>
|
||||
<span className="mt-1 block text-xs text-neutral-500">
|
||||
{t("settings.edit.instagramImport.automaticModeHint")}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowModeChoice(false)}
|
||||
className="text-xs text-neutral-400"
|
||||
>
|
||||
{t("settings.edit.instagramImport.cancelChoice")}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{autoUpdateOn && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTurnOffAutoUpdate}
|
||||
disabled={togglingAutoOff}
|
||||
className="mt-4 text-xs text-neutral-400 disabled:opacity-60"
|
||||
>
|
||||
{t("settings.edit.instagramImport.turnOffAutoUpdate")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleDisconnect}
|
||||
|
||||
168
src/app/settings/edit/instagram-import/select/page.tsx
Normal file
168
src/app/settings/edit/instagram-import/select/page.tsx
Normal file
@@ -0,0 +1,168 @@
|
||||
"use client";
|
||||
|
||||
import Container from "@/components/elements/Container";
|
||||
import PageTitle from "@/components/settings/PageTitle";
|
||||
import LocalePageShell from "@/components/i18n/LocalePageShell";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
type InstagramMediaItem = {
|
||||
id: string;
|
||||
caption: string | null;
|
||||
media_type: "IMAGE" | "VIDEO" | "CAROUSEL_ALBUM";
|
||||
media_url: string;
|
||||
permalink: string;
|
||||
timestamp: string;
|
||||
supported: boolean;
|
||||
already_imported: boolean;
|
||||
};
|
||||
|
||||
export default function InstagramImportSelectPage() {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const { request } = useAxios();
|
||||
const [media, setMedia] = useState<InstagramMediaItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||
const [importing, setImporting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
request<{ media: InstagramMediaItem[] }>("GET", "/auth/instagram/media")
|
||||
.then((res) => setMedia(res?.media || []))
|
||||
.catch(() => toast.error(t("settings.edit.instagramImport.mediaLoadError")))
|
||||
.finally(() => setLoading(false));
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const eligibleIds = media
|
||||
.filter((item) => item.supported && !item.already_imported)
|
||||
.map((item) => item.id);
|
||||
|
||||
const toggleItem = (id: string) => {
|
||||
setSelectedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(id)) next.delete(id);
|
||||
else next.add(id);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSelectAll = () => {
|
||||
setSelectedIds((prev) =>
|
||||
prev.size === eligibleIds.length ? new Set() : new Set(eligibleIds)
|
||||
);
|
||||
};
|
||||
|
||||
const handleImport = async () => {
|
||||
if (selectedIds.size === 0) {
|
||||
toast.error(t("settings.edit.instagramImport.selectAtLeastOne"));
|
||||
return;
|
||||
}
|
||||
setImporting(true);
|
||||
try {
|
||||
const res = await request<{ imported_count: number }>(
|
||||
"POST",
|
||||
"/auth/instagram/import",
|
||||
{ mode: "manual", mediaIds: Array.from(selectedIds) }
|
||||
);
|
||||
toast.success(
|
||||
t("settings.edit.instagramImport.importSuccess", {
|
||||
count: res?.imported_count ?? selectedIds.size,
|
||||
})
|
||||
);
|
||||
router.push("/settings/edit/instagram-import");
|
||||
} catch {
|
||||
toast.error(t("settings.edit.instagramImport.importError"));
|
||||
} finally {
|
||||
setImporting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<LocalePageShell>
|
||||
<Container className="pb-28">
|
||||
<PageTitle>{t("settings.edit.instagramImport.selectPageTitle")}</PageTitle>
|
||||
|
||||
{loading ? (
|
||||
<p className="py-8 text-center text-sm text-neutral-500">
|
||||
{t("common.loading")}
|
||||
</p>
|
||||
) : media.length === 0 ? (
|
||||
<p className="py-8 text-center text-sm text-neutral-500">
|
||||
{t("settings.edit.instagramImport.noMediaFound")}
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
{eligibleIds.length > 0 && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggleSelectAll}
|
||||
className="my-3 text-xs font-semibold text-[#0095f6]"
|
||||
>
|
||||
{selectedIds.size === eligibleIds.length
|
||||
? t("settings.edit.instagramImport.deselectAll")
|
||||
: t("settings.edit.instagramImport.selectAll")}
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-3 gap-1">
|
||||
{media.map((item) => {
|
||||
const disabled = !item.supported || item.already_imported;
|
||||
const selected = selectedIds.has(item.id);
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
onClick={() => toggleItem(item.id)}
|
||||
className={cn(
|
||||
"relative aspect-square overflow-hidden rounded-md",
|
||||
disabled && "opacity-40"
|
||||
)}
|
||||
>
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
src={item.media_url}
|
||||
alt=""
|
||||
className="h-full w-full object-cover"
|
||||
/>
|
||||
{selected && (
|
||||
<div className="absolute inset-0 border-2 border-[#0095f6] bg-[#0095f6]/20" />
|
||||
)}
|
||||
{item.already_imported && (
|
||||
<span className="absolute bottom-1 left-1 right-1 rounded bg-black/60 px-1 py-0.5 text-[10px] text-white">
|
||||
{t("settings.edit.instagramImport.alreadyImportedBadge")}
|
||||
</span>
|
||||
)}
|
||||
{!item.supported && !item.already_imported && (
|
||||
<span className="absolute bottom-1 left-1 right-1 rounded bg-black/60 px-1 py-0.5 text-[10px] text-white">
|
||||
{t("settings.edit.instagramImport.unsupportedTypeBadge")}
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{eligibleIds.length > 0 && (
|
||||
<div className="pointer-events-none fixed bottom-0 left-0 right-0 z-50 flex justify-center px-4 pb-6">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleImport}
|
||||
disabled={importing || selectedIds.size === 0}
|
||||
className="pointer-events-auto w-full max-w-sm rounded-2xl bg-[#0095f6] px-4 py-3 text-sm font-semibold text-white disabled:opacity-50"
|
||||
>
|
||||
{t("settings.edit.instagramImport.importSelectedButton")} ({selectedIds.size})
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</Container>
|
||||
</LocalePageShell>
|
||||
);
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from "@/lib/postLinkCaption";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { useUserById } from "@/hooks/getUserById";
|
||||
import "slick-carousel/slick/slick.css";
|
||||
import "slick-carousel/slick/slick-theme.css";
|
||||
@@ -438,24 +439,7 @@ function MainModelCard({
|
||||
{cuorse_name}
|
||||
<VerificationBadge verifyBadge={verify_badge} />
|
||||
</h2>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{newScore || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt={t("searchPage.starIconAlt")}
|
||||
src="/images/icons/medal-star.png"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{rating || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt={t("searchPage.rateIconAlt")}
|
||||
src="/images/icons/star1.png"
|
||||
/>
|
||||
</div>
|
||||
<ScoreRateStats score={newScore} rate={rating} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
37
src/components/auth/AuthLanguageToggle.tsx
Normal file
37
src/components/auth/AuthLanguageToggle.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
"use client";
|
||||
|
||||
import { useAppLanguage } from "@/contexts/LanguageProvider";
|
||||
import { ENABLED_LANGUAGES, LANGUAGE_LABELS, type AppLanguage } from "@/lib/i18n/registry";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function AuthLanguageToggle({ className }: { className?: string }) {
|
||||
const { language, setLanguage } = useAppLanguage();
|
||||
|
||||
const handleChange = (next: AppLanguage) => {
|
||||
if (next === language) return;
|
||||
void setLanguage(next);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("flex items-center gap-1.5", className)}>
|
||||
{ENABLED_LANGUAGES.map((code) => {
|
||||
const active = language === code;
|
||||
return (
|
||||
<button
|
||||
key={code}
|
||||
type="button"
|
||||
onClick={() => handleChange(code)}
|
||||
className={cn(
|
||||
"rounded-full border px-3 py-1 text-xs font-semibold transition-colors",
|
||||
active
|
||||
? "border-[#0095f6] bg-[#0095f6]/10 text-[#0095f6]"
|
||||
: "border-neutral-200 text-neutral-500 dark:border-neutral-700 dark:text-neutral-400"
|
||||
)}
|
||||
>
|
||||
{LANGUAGE_LABELS[code].native}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -143,17 +143,6 @@ function GoogleSignInInner({
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[290px] mt-5 flex flex-col items-center">
|
||||
<div className="relative flex items-center justify-center mb-4 w-full">
|
||||
<span className="absolute inset-x-0 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
<span className="relative bg-white dark:bg-[#1a1a1a] px-3 text-xs text-gray-500">
|
||||
{mode === "register"
|
||||
? t("auth.orRegisterWith")
|
||||
: mode === "link"
|
||||
? t("settings.edit.googleAccount.orLinkWith")
|
||||
: t("auth.orLoginWith")}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"relative w-full max-w-[290px]",
|
||||
|
||||
@@ -51,13 +51,6 @@ export default function InstagramSignInButton({
|
||||
? t("settings.edit.instagramImport.linkButton")
|
||||
: t("auth.instagramLogin");
|
||||
|
||||
const dividerLabel =
|
||||
mode === "register"
|
||||
? t("auth.orRegisterWithInstagram")
|
||||
: mode === "link"
|
||||
? t("settings.edit.instagramImport.orLinkWith")
|
||||
: t("auth.orLoginWithInstagram");
|
||||
|
||||
const handleClick = () => {
|
||||
const nonce = generateNonce();
|
||||
sessionStorage.setItem("ig_oauth_mode", mode);
|
||||
@@ -71,13 +64,6 @@ export default function InstagramSignInButton({
|
||||
|
||||
return (
|
||||
<div className="w-full max-w-[290px] mt-5 flex flex-col items-center">
|
||||
<div className="relative flex items-center justify-center mb-4 w-full">
|
||||
<span className="absolute inset-x-0 h-px bg-gray-200 dark:bg-gray-700" />
|
||||
<span className="relative bg-white dark:bg-[#1a1a1a] px-3 text-xs text-gray-500">
|
||||
{dividerLabel}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClick}
|
||||
|
||||
29
src/components/main/ScoreRateStats.tsx
Normal file
29
src/components/main/ScoreRateStats.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import Image from "next/image";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
interface ScoreRateStatsProps {
|
||||
score?: string | number | null;
|
||||
rate?: string | number | null;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export default function ScoreRateStats({ score, rate, className }: ScoreRateStatsProps) {
|
||||
return (
|
||||
<span
|
||||
dir="rtl"
|
||||
className={cn(
|
||||
"flex shrink-0 items-center gap-2 text-[11px] font-semibold text-current",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className="pt-[6px] text-sm leading-none tabular-nums">{score || "0"}</span>
|
||||
<Image width={16} height={16} alt="star icon" src="/images/icons/star1.png" />
|
||||
</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className="pt-[6px] text-sm leading-none tabular-nums">{rate || "0"}</span>
|
||||
<Image width={16} height={16} alt="rate icon" src="/images/icons/medal-star.png" />
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -13,6 +13,7 @@ import MainModelCardActions from "./MainModelCardActions";
|
||||
import Link from "next/link";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { useUserById } from "@/hooks/getUserById";
|
||||
import "slick-carousel/slick/slick.css";
|
||||
import "slick-carousel/slick/slick-theme.css";
|
||||
@@ -238,24 +239,7 @@ function MainModelCard({
|
||||
{user_name}
|
||||
<VerificationBadge verifyBadge={verify_badge} />
|
||||
</h2>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{user_score || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt="star icon"
|
||||
src="/images/icons/star1.png"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{rate || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt="rate icon"
|
||||
src="/images/icons/medal-star.png"
|
||||
/>
|
||||
</div>
|
||||
<ScoreRateStats score={user_score} rate={rate} />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="max-sm:text-xs">
|
||||
|
||||
@@ -8,6 +8,7 @@ import Link from "next/link";
|
||||
import CaptionWithMentions from "@/components/posts/CaptionWithMentions";
|
||||
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { useUserById } from "@/hooks/getUserById";
|
||||
import Slider from "react-slick";
|
||||
import "slick-carousel/slick/slick.css";
|
||||
@@ -194,24 +195,7 @@ function MainModelCard({ postData }: { postData: Post }) {
|
||||
{user_name}
|
||||
<VerificationBadge verifyBadge={verify_badge} />
|
||||
</span>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{user_score || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt="star icon"
|
||||
src="/images/icons/medal-star.png"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5 justify-center">
|
||||
<span className="mr-2">{rate || 0}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt="star icon"
|
||||
src="/images/icons/star1.png"
|
||||
/>
|
||||
</div>
|
||||
<ScoreRateStats score={user_score} rate={rate} />
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="max-sm:text-xs">
|
||||
|
||||
@@ -476,6 +476,7 @@ function ModelContent({ user, searchParams }: ModelContentProps) {
|
||||
_id={user?._id}
|
||||
token={token}
|
||||
privateLocked={Boolean(user?.private_content_locked)}
|
||||
profileUser={user}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
"use client";
|
||||
|
||||
import React, { useEffect, useState, useMemo, useCallback } from "react";
|
||||
import React, { useMemo, useCallback } from "react";
|
||||
import { navigateWithSavedScroll } from "@/lib/listScrollRestoration";
|
||||
import { useListScrollRestoration } from "@/hooks/useListScrollRestoration";
|
||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { fetchPosts } from "@/api/fetchPosts";
|
||||
import { Post, User } from "@/types/types";
|
||||
import axios from "axios";
|
||||
import { BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import { buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import Image from "next/image";
|
||||
import BoldIcon from "@/components/ui/BoldIcon";
|
||||
import { ChatListSkeleton } from "@/components/ui/ChatSkeletons";
|
||||
@@ -22,6 +21,7 @@ interface InfinitePostsProps {
|
||||
token: string;
|
||||
_id: string;
|
||||
privateLocked?: boolean;
|
||||
profileUser?: User;
|
||||
filters: {
|
||||
expertise?: string;
|
||||
province?: string;
|
||||
@@ -36,27 +36,10 @@ function ModelContentPosts({
|
||||
token,
|
||||
_id,
|
||||
privateLocked = false,
|
||||
profileUser,
|
||||
}: InfinitePostsProps) {
|
||||
const { t } = useTranslation("common");
|
||||
const router = useRouter();
|
||||
const [allUsers, setAllUsers] = useState<User[]>([]);
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const response = await axios.get(`${BASE_URL}/users?limit=1000`, {
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
});
|
||||
if (response?.data?.users) {
|
||||
setAllUsers(response.data.users);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("خطا در دریافت کاربران:", err);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const filterKey = useMemo(() => JSON.stringify({ ...filters, _id }), [filters, _id]);
|
||||
|
||||
@@ -111,7 +94,7 @@ function ModelContentPosts({
|
||||
const posts: Post[] = data?.pages?.flatMap((page) => page.posts) ?? [];
|
||||
|
||||
const filteredPosts = posts.filter((post) => {
|
||||
const user = allUsers.find((u) => u._id === post.userId);
|
||||
const user = profileUser?._id === post.userId ? profileUser : undefined;
|
||||
if (!user) return true;
|
||||
|
||||
let include = true;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useUserById } from "@/hooks/getUserById";
|
||||
import VerificationBadge from "@/components/main/VerificationBadge";
|
||||
import Image from "next/image";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import React from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
@@ -32,29 +32,8 @@ function ModelHeadRowTwo({
|
||||
const verify_badge = verifyBadgeProp ?? user?.verify_badge;
|
||||
const is_Register = isRegisterProp ?? user?.is_Register;
|
||||
return (
|
||||
<div className="py-2 grid w-full grid-cols-3 items-end text-xs md:text-sm font-semibold">
|
||||
<div className="flex items-center">
|
||||
<>
|
||||
<span>{user_score ? user_score : "0"}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt={"star icon"}
|
||||
src={`/images/icons/star1.png`}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<>
|
||||
<span>{rate ? rate : "0"}</span>
|
||||
<Image
|
||||
width={22}
|
||||
height={22}
|
||||
alt={"rate icon"}
|
||||
src={`/images/icons/medal-star.png`}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<div className="py-2 flex w-full items-end justify-between text-xs md:text-sm font-semibold">
|
||||
<ScoreRateStats score={user_score} rate={rate} />
|
||||
<div
|
||||
className={cn(
|
||||
"flex min-w-0 max-w-full flex-col",
|
||||
|
||||
@@ -4,10 +4,10 @@ import Modal from "@/components/elements/Modal";
|
||||
import RoundedButton from "@/components/elements/RoundedButton";
|
||||
import RoundedDiv from "@/components/elements/RoundedDiv";
|
||||
import UserInfo from "@/components/main/UserInfo";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { buildStorageUrl } from "@/components/main/BaseUrl";
|
||||
import useAxios from "@/hooks/useAxios";
|
||||
import { IProjectRequest } from "@/types/types";
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -77,17 +77,22 @@ function ProjectRequestsAction({
|
||||
setShowConfirmModal(true);
|
||||
}}
|
||||
>
|
||||
<UserInfo
|
||||
first_name={item?.user?.first_name}
|
||||
is_verified={item?.user?.verify_badge}
|
||||
userId={item?.user?._id}
|
||||
profile_image={item?.user?.profile_image}
|
||||
user_level={item?.user?.user_level}
|
||||
last_name={item?.user?.last_name}
|
||||
user_name={item?.user?.user_name}
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<UserInfo
|
||||
first_name={item?.user?.first_name}
|
||||
is_verified={item?.user?.verify_badge}
|
||||
userId={item?.user?._id}
|
||||
profile_image={item?.user?.profile_image}
|
||||
user_level={item?.user?.user_level}
|
||||
last_name={item?.user?.last_name}
|
||||
user_name={item?.user?.user_name}
|
||||
trailingContent={
|
||||
<ScoreRateStats score={item?.user?.user_score} rate={item?.user?.rate} />
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-start gap-1.5 text-[10px] sm:text-xs">
|
||||
<div className="flex shrink-0 flex-col items-start gap-1.5 text-[10px] sm:text-xs">
|
||||
<span>
|
||||
{t("projects.workroom.offerPrice", {
|
||||
amount: Number(item?.price).toLocaleString(),
|
||||
@@ -103,30 +108,6 @@ function ProjectRequestsAction({
|
||||
})}
|
||||
</span>
|
||||
) : null}
|
||||
<div className="flex gap-3">
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<span>
|
||||
{item?.user?.user_score ? item?.user?.user_score : "0"}
|
||||
</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt=""
|
||||
src={`/images/icons/medal-star.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<span>{item?.user?.rate ? item?.user?.rate : "0"}</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt=""
|
||||
src={`/images/icons/star1.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import UserInfo from "@/components/main/UserInfo";
|
||||
import ScoreRateStats from "@/components/main/ScoreRateStats";
|
||||
import { SelectedUser } from "@/types/types";
|
||||
import Image from "next/image";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
@@ -27,35 +27,10 @@ function ProjectWorkroomSelectedUser({
|
||||
user_level={selectedUser?.user_level}
|
||||
last_name={selectedUser?.last_name}
|
||||
user_name={selectedUser?.user_name}
|
||||
trailingContent={
|
||||
<ScoreRateStats score={selectedUser?.user_score} rate={selectedUser?.rate} />
|
||||
}
|
||||
/>
|
||||
<div className="flex gap-3">
|
||||
<div className="flex items-center justify-center gap-0.5">
|
||||
<>
|
||||
<span>
|
||||
{selectedUser?.user_score ? selectedUser?.user_score : "0"}
|
||||
</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt={"verify icon"}
|
||||
src={`/images/icons/medal-star.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
<div className="flex items-center gap-0.5">
|
||||
<>
|
||||
<span>{selectedUser?.rate ? selectedUser?.rate : "0"}</span>
|
||||
<Image
|
||||
width={21}
|
||||
height={21}
|
||||
alt={"verify icon"}
|
||||
src={`/images/icons/star1.png`}
|
||||
className="pb-1"
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { User } from "@/types/types";
|
||||
import React from "react";
|
||||
import UserInfo from "../main/UserInfo";
|
||||
import Image from "next/image";
|
||||
import ScoreRateStats from "../main/ScoreRateStats";
|
||||
import Link from "next/link";
|
||||
|
||||
function SearchCard({ user }: { user: User }) {
|
||||
@@ -19,18 +19,7 @@ function SearchCard({ user }: { user: User }) {
|
||||
last_name={user?.last_name}
|
||||
user_name={user?.user_name}
|
||||
noLink
|
||||
trailingContent={
|
||||
<span className="flex shrink-0 items-center gap-2 text-[11px] font-normal text-neutral-500 dark:text-neutral-400">
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className="pt-[6px] text-sm leading-none">{user?.user_score ? user?.user_score : "0"}</span>
|
||||
<Image width={16} height={16} alt="star icon" src="/images/icons/star1.png" />
|
||||
</span>
|
||||
<span className="flex items-center gap-0.5">
|
||||
<span className="pt-[6px] text-sm leading-none">{user?.rate ? user?.rate : "0"}</span>
|
||||
<Image width={16} height={16} alt="rate icon" src="/images/icons/medal-star.png" />
|
||||
</span>
|
||||
</span>
|
||||
}
|
||||
trailingContent={<ScoreRateStats score={user?.user_score} rate={user?.rate} />}
|
||||
/>
|
||||
</Link>
|
||||
);
|
||||
|
||||
@@ -5,10 +5,13 @@
|
||||
*/
|
||||
export const INSTAGRAM_OAUTH_CLIENT_ID = "";
|
||||
|
||||
/** Must exactly match INSTAGRAM_REDIRECT_URI configured on the backend and in the Meta App. */
|
||||
/**
|
||||
* Fixed (not derived from window.location) so it exactly matches
|
||||
* INSTAGRAM_REDIRECT_URI on the backend and the URI registered in the Meta App,
|
||||
* regardless of whether the user is on modstagram.com or www.modstagram.com.
|
||||
*/
|
||||
export function getInstagramRedirectUri(): string {
|
||||
if (typeof window === "undefined") return "";
|
||||
return `${window.location.origin}/auth/instagram/callback`;
|
||||
return "https://modstagram.com/auth/instagram/callback";
|
||||
}
|
||||
|
||||
export type InstagramAuthMode = "login" | "register" | "link";
|
||||
|
||||
@@ -397,7 +397,33 @@
|
||||
"unlinkedHint": "Only Instagram Business or Creator accounts can connect. Once connected, you can import your bio, posts, reels, and profile picture from Instagram.",
|
||||
"disconnectButton": "Disconnect Instagram",
|
||||
"disconnectSuccess": "Instagram account disconnected",
|
||||
"disconnectError": "Could not disconnect Instagram account"
|
||||
"disconnectError": "Could not disconnect Instagram account",
|
||||
"updateButton": "Update Instagram posts",
|
||||
"chooseModeTitle": "Choose how to import your posts",
|
||||
"manualMode": "Manual",
|
||||
"manualModeHint": "Pick which of your Instagram posts to bring over to Modstagram",
|
||||
"automaticMode": "Automatic",
|
||||
"automaticModeHint": "Your new Instagram posts will be added to Modstagram automatically",
|
||||
"autoUpdateOnStatus": "Automatic sync: on",
|
||||
"turnOffAutoUpdate": "Turn off automatic sync",
|
||||
"autoUpdateOffSuccess": "Automatic sync turned off",
|
||||
"importSuccess": "{{count}} posts imported",
|
||||
"importError": "Error importing posts",
|
||||
"automaticStartSuccess": "Automatic sync turned on",
|
||||
"selectPageTitle": "Select posts",
|
||||
"selectAll": "Select all",
|
||||
"deselectAll": "Deselect all",
|
||||
"importSelectedButton": "Import selected posts",
|
||||
"noMediaFound": "No posts found",
|
||||
"alreadyImportedBadge": "Already imported",
|
||||
"unsupportedTypeBadge": "Not supported",
|
||||
"selectAtLeastOne": "Select at least one post",
|
||||
"mediaLoadError": "Error loading Instagram posts",
|
||||
"cancelChoice": "Cancel",
|
||||
"dataDeletionStatusTitle": "Instagram data deletion status",
|
||||
"dataDeletionStatusDone": "Your request to delete data received from Instagram (connection token, imported posts, and Instagram profile info) has been completed.",
|
||||
"dataDeletionStatusCode": "Confirmation code",
|
||||
"dataDeletionInstructions": "To delete the data Modstagram received from your Instagram account (connection token, imported posts, and profile info), go to Settings > Import from Instagram > Disconnect inside Modstagram, or remove Modstagram's access from Instagram's Settings > Apps and websites. Either way, this data is deleted automatically."
|
||||
},
|
||||
"twoFactor": {
|
||||
"loadError": "Could not load two-factor status",
|
||||
@@ -619,6 +645,8 @@
|
||||
"instagramLoginFailed": "Instagram sign-in failed",
|
||||
"instagramAccountTypeError": "Only Instagram Business or Creator accounts can connect.",
|
||||
"usernameSuggestedFromInstagram": "This username was suggested from Instagram; you can change it",
|
||||
"nameSuggestedFromInstagram": "This name was suggested from Instagram; you can change it",
|
||||
"instagramContinuingToProfile": "Your name was saved. Continuing to the next step...",
|
||||
"orLoginWith": "Or continue with",
|
||||
"orRegisterWith": "Or sign up with",
|
||||
"twoFactorTitle": "Two-factor authentication",
|
||||
|
||||
@@ -397,7 +397,33 @@
|
||||
"unlinkedHint": "فقط حسابهای Business یا Creator اینستاگرام قابل اتصال هستند. پس از اتصال میتوانید بیو، پستها، ریلزها و عکس پروفایل خود را از اینستاگرام وارد کنید.",
|
||||
"disconnectButton": "قطع اتصال اینستاگرام",
|
||||
"disconnectSuccess": "اتصال اینستاگرام قطع شد",
|
||||
"disconnectError": "خطا در قطع اتصال اینستاگرام"
|
||||
"disconnectError": "خطا در قطع اتصال اینستاگرام",
|
||||
"updateButton": "بروزرسانی پستهای اینستاگرام",
|
||||
"chooseModeTitle": "روش انتقال پستها را انتخاب کنید",
|
||||
"manualMode": "دستی",
|
||||
"manualModeHint": "از بین پستهای اینستاگرامتان انتخاب کنید کدامها به مدستاگرام منتقل شوند",
|
||||
"automaticMode": "خودکار",
|
||||
"automaticModeHint": "پستهای جدید اینستاگرام شما بهطور خودکار به مدستاگرام اضافه میشوند",
|
||||
"autoUpdateOnStatus": "همگامسازی خودکار: فعال",
|
||||
"turnOffAutoUpdate": "غیرفعال کردن همگامسازی خودکار",
|
||||
"autoUpdateOffSuccess": "همگامسازی خودکار غیرفعال شد",
|
||||
"importSuccess": "{{count}} پست منتقل شد",
|
||||
"importError": "خطا در انتقال پستها",
|
||||
"automaticStartSuccess": "همگامسازی خودکار فعال شد",
|
||||
"selectPageTitle": "انتخاب پستها",
|
||||
"selectAll": "انتخاب همه",
|
||||
"deselectAll": "لغو انتخاب همه",
|
||||
"importSelectedButton": "انتقال پستهای انتخابشده",
|
||||
"noMediaFound": "پستی برای نمایش یافت نشد",
|
||||
"alreadyImportedBadge": "قبلاً منتقل شده",
|
||||
"unsupportedTypeBadge": "پشتیبانی نمیشود",
|
||||
"selectAtLeastOne": "حداقل یک پست انتخاب کنید",
|
||||
"mediaLoadError": "خطا در دریافت پستهای اینستاگرام",
|
||||
"cancelChoice": "انصراف",
|
||||
"dataDeletionStatusTitle": "وضعیت حذف دادههای اینستاگرام",
|
||||
"dataDeletionStatusDone": "درخواست حذف دادههای دریافتشده از اینستاگرام (توکن اتصال، پستهای واردشده و اطلاعات پروفایل اینستاگرام) با موفقیت انجام شد.",
|
||||
"dataDeletionStatusCode": "کد پیگیری",
|
||||
"dataDeletionInstructions": "برای حذف دادههایی که مدستاگرام از حساب اینستاگرام شما دریافت کرده (توکن اتصال، پستهای واردشده و اطلاعات پروفایل)، میتوانید از داخل مدستاگرام در مسیر «تنظیمات > وارد کردن از اینستاگرام > قطع اتصال» اقدام کنید، یا از داخل اینستاگرام در مسیر «تنظیمات > برنامهها و وبسایتها» دسترسی مدستاگرام را حذف کنید؛ در هر دو حالت این دادهها بهصورت خودکار حذف میشوند."
|
||||
},
|
||||
"twoFactor": {
|
||||
"loadError": "خطا در دریافت وضعیت ورود دو مرحلهای",
|
||||
@@ -621,6 +647,8 @@
|
||||
"instagramLoginFailed": "ورود با اینستاگرام ناموفق بود",
|
||||
"instagramAccountTypeError": "فقط حسابهای Business یا Creator اینستاگرام قابل اتصال هستند.",
|
||||
"usernameSuggestedFromInstagram": "این نام کاربری از اینستاگرام پیشنهاد شده؛ میتوانید تغییرش دهید",
|
||||
"nameSuggestedFromInstagram": "این نام از اینستاگرام پیشنهاد شده؛ میتوانید تغییرش دهید",
|
||||
"instagramContinuingToProfile": "نام شما ثبت شد. در حال انتقال به مرحلهی بعد...",
|
||||
"twoFactorTitle": "ورود دو مرحلهای",
|
||||
"twoFactorHint": "کد ۶ رقمی Google Authenticator را وارد کنید",
|
||||
"mobileInvalid": "شماره موبایل معتبر نیست",
|
||||
|
||||
Reference in New Issue
Block a user