This commit is contained in:
payacom
2026-08-14 21:31:11 +03:30
parent a92a3ca5e6
commit c879976c56
176 changed files with 30270 additions and 1441 deletions

View File

@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M8.9101 20.67C8.7201 20.67 8.5301 20.6 8.3801 20.45C8.0901 20.16 8.0901 19.68 8.3801 19.39L14.9001 12.87C15.3801 12.39 15.3801 11.61 14.9001 11.13L8.3801 4.60999C8.0901 4.31999 8.0901 3.83999 8.3801 3.54999C8.6701 3.25999 9.1501 3.25999 9.4401 3.54999L15.9601 10.07C16.4701 10.58 16.7601 11.27 16.7601 12C16.7601 12.73 16.4801 13.42 15.9601 13.93L9.4401 20.45C9.2901 20.59 9.1001 20.67 8.9101 20.67Z" fill="#292D32"/>
</svg>

After

Width:  |  Height:  |  Size: 530 B

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 24 24" style="enable-background:new 0 0 24 24;" xml:space="preserve">
<style type="text/css">
.st0{fill:#FF0000;}
</style>
<path class="st0" d="M21.6,10.7l-1.4-1.6c-0.3-0.3-0.5-0.9-0.5-1.3V6.2c0-1.1-0.9-1.9-1.9-1.9h-1.7c-0.4,0-1-0.2-1.3-0.5l-1.6-1.4
c-0.7-0.6-1.8-0.6-2.5,0L9.2,3.8C8.9,4.1,8.3,4.3,7.9,4.3H6.2c-1.1,0-1.9,0.9-1.9,1.9v1.7c0,0.4-0.2,0.9-0.5,1.2l-1.3,1.6
c-0.6,0.7-0.6,1.8,0,2.5l1.3,1.6c0.2,0.3,0.5,0.9,0.5,1.2v1.7c0,1.1,0.9,1.9,1.9,1.9h1.7c0.4,0,1,0.2,1.3,0.5l1.6,1.4
c0.7,0.6,1.8,0.6,2.5,0l1.6-1.4c0.3-0.3,0.9-0.5,1.3-0.5h1.7c1.1,0,1.9-0.9,1.9-1.9v-1.7c0-0.4,0.2-1,0.5-1.3l1.4-1.6
C22.1,12.6,22.1,11.4,21.6,10.7z"/>
</svg>

After

Width:  |  Height:  |  Size: 823 B

View File

@@ -100,7 +100,7 @@ function ChangePassword() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.changePasswordTitle")} />
<p className="text-sm text-gray-500 text-center mb-4 max-w-sm">
{isEmailReset

View File

@@ -74,7 +74,7 @@ function ForgetPasswordOtp() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.forgetPasswordTitle")} />
<AuthOtpStatus
mobile={mobile}

View File

@@ -76,7 +76,7 @@ function ForgetPasswordPage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.forgetPasswordTitle")} />
<p className="text-sm text-gray-500 text-center mb-4 max-w-sm">
{t("auth.forgetPasswordDesc")}

View File

@@ -58,7 +58,7 @@ export default function LoginTwoFactorPage() {
return (
<LocalePageShell>
<Container>
<div className="flex h-screen flex-col items-center justify-center p-4">
<div className="flex h-dvh flex-col items-center justify-center p-4">
<AuthHead title={t("auth.twoFactorTitle")} />
<p className="mb-6 max-w-sm text-center text-sm text-neutral-500">
{t("auth.twoFactorHint")}

View File

@@ -95,7 +95,7 @@ function LoginWithUsername() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.login")} />
<form
onSubmit={formik.handleSubmit}

View File

@@ -13,7 +13,7 @@ import * as yup from "yup";
import { useRouter } from "next/navigation";
import { markOtpSent } from "@/lib/auth/otpTimer";
import { getSafeRedirectPath } from "@/lib/auth/postLogin";
import { GoogleSignInButton, InstagramSignInButton } from "@/app/(auth)/AuthProviders";
import { GoogleSignInButton } from "@/app/(auth)/AuthProviders";
import { MOBILE_NUMERIC_INPUT_PROPS } from "@/lib/auth/inputProps";
import { useAuthSessionRedirect } from "@/hooks/useAuthSessionRedirect";
import { useTranslation } from "react-i18next";
@@ -61,7 +61,7 @@ function Login() {
return (
<LocalePageShell>
<Container>
<div className="relative p-4 flex flex-col items-center h-screen justify-center">
<div className="relative p-4 flex flex-col items-center h-dvh justify-center">
<AuthLanguageToggle className="absolute top-6 left-1/2 -translate-x-1/2" />
<AuthHead title={t("auth.login")} />
<form
@@ -93,7 +93,7 @@ function Login() {
</AuthButton>
</form>
<GoogleSignInButton mode="login" />
<InstagramSignInButton mode="login" />
{/* <InstagramSignInButton mode="login" /> */}
<Link href={usernameLoginHref}>
<small className="text-[#FF5C00] font-bold text-xs mt-8 block">
{t("auth.loginWithUsername")}

View File

@@ -79,7 +79,7 @@ function VerifyOtp() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.login")} />
<AuthOtpStatus
mobile={mobile}

View File

@@ -72,7 +72,7 @@ function RegisterOtp() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<AuthHead title={t("auth.register")} />
<AuthOtpStatus
mobile={mobile}

View File

@@ -11,7 +11,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import { markOtpSent } from "@/lib/auth/otpTimer";
import { GoogleSignInButton, InstagramSignInButton } from "@/app/(auth)/AuthProviders";
import { GoogleSignInButton } from "@/app/(auth)/AuthProviders";
import { MOBILE_NUMERIC_INPUT_PROPS } from "@/lib/auth/inputProps";
import { getAxiosErrorMessage } from "@/lib/auth/postLogin";
import { useTranslation } from "react-i18next";
@@ -85,7 +85,7 @@ function Register() {
return (
<LocalePageShell>
<Container>
<div className="relative p-4 flex flex-col items-center h-screen justify-center">
<div className="relative p-4 flex flex-col items-center h-dvh justify-center">
<AuthLanguageToggle className="absolute top-6 left-1/2 -translate-x-1/2" />
<AuthHead title={t("auth.register")} />
<form
@@ -127,7 +127,7 @@ function Register() {
</AuthButton>
</form>
<GoogleSignInButton mode="register" />
<InstagramSignInButton mode="register" />
{/* <InstagramSignInButton mode="register" /> */}
<Link href="/login">
<small className="text-[#292D32] dark:text-neutral-300 font-bold text-xs mt-6 block text-center">
{t("auth.haveAccount")}{" "}

View File

@@ -1,261 +1,198 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useEffect, useMemo, useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import SelectBox from "@/components/elements/SelectBox";
import { ICity, IProvince } from "@/types/types";
import Map, { GeolocateControl, Marker } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import LocalePageShell from "@/components/i18n/LocalePageShell";
function LocationPage() {
const { t } = useTranslation("common");
const router = useRouter();
const { request, loading } = useAxios();
const [isCheckedOne, setIsCheckedOne] = useState(false);
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [selectedLocation, setSelectedLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
const schema = useMemo(
() =>
yup.object().shape({
markerCoordinate: yup.mixed().required(t("auth.locationRequired")),
address: yup.string().required(t("auth.addressRequired")),
cityId: yup.string().required(t("auth.cityRequired")),
stateId: yup.string().required(t("auth.provinceRequired")),
}),
[t]
);
const fetchStates = async () => {
try {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
);
setAllStates(response?.provinces || null);
} catch (err) {
console.log(err);
}
};
const fetchCities = async (provinceId: string) => {
try {
const response = await request<{ cities: ICity[] }>(
"GET",
`/cities/${provinceId}`
);
setCities(response?.cities || []);
} catch (err) {
console.log(err);
}
};
useEffect(() => {
fetchStates();
}, []);
const toggleCheckBox = () => {
setIsCheckedOne(!isCheckedOne);
};
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
const formik = useFormik({
initialValues: {
markerCoordinate: [],
address: "",
cityId: "",
stateId: "",
},
validationSchema: schema,
onSubmit: async (values, { setSubmitting }) => {
try {
await request("POST", "/verify/address", {
mobile: mobile,
address: values.address,
province_id: values.stateId,
city_id: values.cityId,
lat: values.markerCoordinate[1],
lng: values.markerCoordinate[0],
show_location: isCheckedOne,
});
router.push("/verify/national-cart");
} catch (err: any) {
toast.error(err?.response?.data?.message || t("auth.unknownError"));
} finally {
setSubmitting(false);
}
},
});
const handleProvinceChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedProvinceId = e.target.value;
formik.setFieldValue("stateId", selectedProvinceId);
fetchCities(selectedProvinceId);
};
const handleMapClick = (event: any) => {
const { lngLat } = event;
setSelectedLocation({
lat: lngLat.lat,
lng: lngLat.lng,
});
formik.setFieldValue("markerCoordinate", [lngLat.lng, lngLat.lat]);
};
return (
<LocalePageShell>
<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">{t("auth.locationTitle")}</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<small className="font-bold mt-10 mb-4">
{t("auth.locationActivityHint")}
</small>
<div className="w-full max-w-sm flex flex-col items-center">
<SelectBox
className={`mt-4 ${
formik.touched.stateId && formik.errors.stateId
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.stateId}
onChange={handleProvinceChange}
>
<option disabled value="">
{t("auth.province")}
</option>
{allStates?.map((item: IProvince) => (
<option key={item.id} value={item.id}>
{item?.name}
</option>
))}
</SelectBox>
{formik.touched.stateId && formik.errors.stateId && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.stateId}
</small>
)}
<SelectBox
className={`mt-4 ${
formik.touched.cityId && formik.errors.cityId
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.cityId}
onChange={(e) => formik.setFieldValue("cityId", e.target.value)}
>
<option disabled value="">
{t("auth.city")}
</option>
{cities?.map((city: ICity) => (
<option key={city.id} value={city.id}>
{city.name}
</option>
))}
</SelectBox>
{formik.touched.cityId && formik.errors.cityId && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.cityId}
</small>
)}
<textarea
name="address"
placeholder={t("auth.addressPlaceholder")}
className={`border mt-4 w-full max-w-[300px] p-3 rounded-2xl border-border-primary-light dark:border-border-primary-dark bg-secondary-light dark:bg-secondary-dark font-medium ${
formik.touched.address && formik.errors.address
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.address && formik.errors.address && (
<small className="text-red-500 mt-2 block">
{formik.errors.address}
</small>
)}
<Map
style={{ height: "calc(100vh - 260px)" }}
initialViewState={{
longitude: 51.375433528216654,
latitude: 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{selectedLocation && (
<Marker
latitude={selectedLocation.lat}
longitude={selectedLocation.lng}
>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src={"/images/icons/location.svg"}
/>
</Marker>
)}
</Map>
<label className="flex items-center gap-2">
<input
className="scale-125"
type="checkbox"
checked={isCheckedOne}
onChange={toggleCheckBox}
/>
<span className="text-xs font-bold">
{t("auth.showLocationPublic")}
</span>
</label>
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/national-cart")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
{t("auth.saveAndContinue")}
</AuthNextButton>
</AuthFormFooter>
</form>
</AuthPageLayout>
</LocalePageShell>
);
}
export default LocationPage;
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import React, { useMemo, useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import CountryProvinceCitySelect, {
type GeoSelection,
countryHasProvinces,
} from "@/components/ui/CountryProvinceCitySelect";
import Map, { GeolocateControl, Marker } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import Image from "next/image";
import { useTranslation } from "react-i18next";
import LocalePageShell from "@/components/i18n/LocalePageShell";
function LocationPage() {
const { t } = useTranslation("common");
const router = useRouter();
const { request, loading } = useAxios();
const [isCheckedOne, setIsCheckedOne] = useState(false);
const [geoSelection, setGeoSelection] = useState<GeoSelection>({
countryId: null,
provinceId: null,
cityId: null,
});
const [selectedLocation, setSelectedLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
const schema = useMemo(
() =>
yup.object().shape({
markerCoordinate: yup.mixed().required(t("auth.locationRequired")),
address: yup.string().required(t("auth.addressRequired")),
}),
[t]
);
const mobile =
typeof window !== "undefined" ? localStorage.getItem("mobile") : null;
const formik = useFormik({
initialValues: {
markerCoordinate: [],
address: "",
},
validationSchema: schema,
onSubmit: async (values, { setSubmitting }) => {
const provinceRequired = countryHasProvinces(geoSelection.countryId);
if (provinceRequired && !geoSelection.provinceId) {
toast.error(t("auth.provinceRequired"));
setSubmitting(false);
return;
}
if (provinceRequired && !geoSelection.cityId) {
toast.error(t("auth.cityRequired"));
setSubmitting(false);
return;
}
try {
await request("POST", "/verify/address", {
mobile: mobile,
address: values.address,
country_id: geoSelection.countryId,
province_id: geoSelection.provinceId,
city_id: geoSelection.cityId,
lat: values.markerCoordinate[1],
lng: values.markerCoordinate[0],
show_location: isCheckedOne,
});
router.push("/verify/national-cart");
} catch (err: any) {
toast.error(err?.response?.data?.message || t("auth.unknownError"));
} finally {
setSubmitting(false);
}
},
});
const toggleCheckBox = () => {
setIsCheckedOne(!isCheckedOne);
};
const handleMapClick = (event: any) => {
const { lngLat } = event;
setSelectedLocation({
lat: lngLat.lat,
lng: lngLat.lng,
});
formik.setFieldValue("markerCoordinate", [lngLat.lng, lngLat.lat]);
};
return (
<LocalePageShell>
<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">{t("auth.locationTitle")}</span>
<div className="mt-5">
<AuthUserDetails />
</div>
<small className="font-bold mt-10 mb-4">
{t("auth.locationActivityHint")}
</small>
<div className="w-full max-w-sm flex flex-col items-center">
<CountryProvinceCitySelect
value={geoSelection}
onChange={setGeoSelection}
className="mt-4 w-full"
/>
<textarea
name="address"
placeholder={t("auth.addressPlaceholder")}
className={`border mt-4 w-full max-w-[300px] p-3 rounded-2xl border-border-primary-light dark:border-border-primary-dark bg-secondary-light dark:bg-secondary-dark font-medium ${
formik.touched.address && formik.errors.address
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.address && formik.errors.address && (
<small className="text-red-500 mt-2 block">
{formik.errors.address}
</small>
)}
<Map
style={{ height: "calc(100dvh - 260px)" }}
initialViewState={{
longitude: 51.375433528216654,
latitude: 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{selectedLocation && (
<Marker
latitude={selectedLocation.lat}
longitude={selectedLocation.lng}
>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src={"/images/icons/location.svg"}
/>
</Marker>
)}
</Map>
<label className="flex items-center gap-2">
<input
className="scale-125"
type="checkbox"
checked={isCheckedOne}
onChange={toggleCheckBox}
/>
<span className="text-xs font-bold">
{t("auth.showLocationPublic")}
</span>
</label>
</div>
</AuthPageContent>
<AuthFormFooter onSkip={() => router.push("/verify/national-cart")}>
<AuthNextButton type="submit" loading={loading} disabled={loading}>
{t("auth.saveAndContinue")}
</AuthNextButton>
</AuthFormFooter>
</form>
</AuthPageLayout>
</LocalePageShell>
);
}
export default LocationPage;

View File

@@ -23,7 +23,7 @@ import { cn } from "@/lib/utils";
import { Course } from "@/types/types";
import useAxios from "@/hooks/useAxios";
import { Comments } from "@/components/academy/CommentsModal";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { buildStorageUrl } from "@/components/main/BaseUrl";
import VerificationBadge from "@/components/main/VerificationBadge";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { usePathname } from "next/navigation";
@@ -639,7 +639,7 @@ useEffect(() => {
}
return (
<div className="min-h-screen bg-white dark:bg-neutral-950">
<div className="min-h-dvh bg-white dark:bg-neutral-950">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* سمت راست - پخش ویدیو و اطلاعات */}
@@ -826,7 +826,7 @@ useEffect(() => {
<div className="relative h-48 bg-gradient-to-r from-[#FF107D] to-[#FF107D50]">
{courses[0]?.course_image && (
<Image
src={IMAGE_BASE_URL + courses[0]?.course_image}
src={buildStorageUrl(courses[0]?.course_image)}
alt={courses[0]?.cuorse_name || "course"}
fill
className="object-cover"

View File

@@ -8,7 +8,7 @@ import {
AcademyProfileHeadSkeleton,
} from "@/components/academy/AcademySkeletons";
import Container from "@/components/elements/Container";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import { buildStorageUrl } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import { useUserById } from "@/hooks/getUserById";
import { Course, IContactInfo } from "@/types/types";
@@ -247,7 +247,7 @@ export default function AcademyProfileClient() {
<ProfileAvatar
src={
academy.academy_image
? `${IMAGE_BASE_URL}${academy.academy_image}`
? buildStorageUrl(academy.academy_image)
: undefined
}
alt={academyName}

View File

@@ -5,6 +5,7 @@ import MainProjectCard from "@/components/projects/MainProjectCard";
import ProjectRequestForm from "@/components/projects/ProjectPage/ProjectRequestForm";
import ProjectRequests from "@/components/projects/ProjectPage/ProjectRequests";
import PageLoader from "@/components/ui/PageLoader";
import BackButton from "@/components/ui/BackButton";
import useAxios from "@/hooks/useAxios";
import { Project, IProjectRequest, ProjectRole } from "@/types/types";
import { useEffect, useMemo, useState } from "react";
@@ -74,6 +75,7 @@ export default function ProjectDetailClient({ id }: { id: string }) {
return (
<Container className="pb-28">
<BackButton />
<MainProjectCard
project={project}
full

View File

@@ -8,6 +8,7 @@ import MainProjectCard from "@/components/projects/MainProjectCard";
import RoundedDiv from "@/components/elements/RoundedDiv";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { useTranslation } from "react-i18next";
import toast from "react-hot-toast";
import { AxiosError } from "axios";
@@ -117,6 +118,7 @@ function ProjectPaymentPage({ params }: ProjectPaymentProps) {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="mb-4 mt-2 line-clamp-2 text-center text-lg font-bold text-text-blue-light dark:text-text-blue-dark md:text-xl">
{t("projects.payment.title")}
</h6>

View File

@@ -11,6 +11,7 @@ import Image from "next/image";
import MainProjectCard from "@/components/projects/MainProjectCard";
import { useTranslation } from "react-i18next";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import toast from "react-hot-toast";
function FailedProject() {
@@ -99,6 +100,7 @@ function FailedProject() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="mb-4 mt-2 line-clamp-2 text-center text-lg font-bold text-[#FF0000] md:text-xl">
{t("projects.payment.failed")}
</h6>

View File

@@ -12,6 +12,7 @@ import Link from "next/link";
import MainProjectCard from "@/components/projects/MainProjectCard";
import { useTranslation } from "react-i18next";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
function SuccessProject() {
const { t } = useTranslation("common");
@@ -103,6 +104,7 @@ function SuccessProject() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="mb-4 mt-2 line-clamp-2 text-center text-lg font-bold text-[#17A600] md:text-xl">
{t("projects.payment.success")}
</h6>

View File

@@ -7,6 +7,7 @@ import { useRouter } from "next/navigation";
import { AnimatePresence, motion, PanInfo } from "framer-motion";
import FilterModal from "@/components/models/FilterModal";
import BoldIcon from "@/components/ui/BoldIcon";
import BackButton from "@/components/ui/BackButton";
import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import useAxios from "@/hooks/useAxios";
@@ -328,14 +329,7 @@ export default function SpecialistsPage() {
<LocalePageShell>
<Container className="pb-28">
<div className="mb-4 flex items-center justify-between px-1 pt-2">
<button
type="button"
onClick={() => router.push("/projects")}
className="flex h-9 w-9 items-center justify-center"
aria-label={t("common.back")}
>
<BoldIcon name="arrow-right" size={22} />
</button>
<BackButton href="/projects" />
<h1 className="text-base font-bold">
{t("projects.specialists.title")}
</h1>

View File

@@ -2,6 +2,7 @@
import Container from "@/components/elements/Container";
import RoundedDiv from "@/components/elements/RoundedDiv";
import EditPageHeader from "@/components/settings/EditPageHeader";
import Link from "next/link";
import { useTranslation } from "react-i18next";
@@ -12,7 +13,7 @@ function AboutUs() {
<Container>
<div className="min-h-[500px] flex flex-col justify-around max-w-md mx-auto px-4">
<div className="">
<h6 className="text-center mt-10 font-bold text-xl">{t("aboutPage.title")}</h6>
<EditPageHeader title={t("aboutPage.title")} className="mt-10" />
<div className="flex flex-col w-full mt-20 gap-1 ">
<span>{t("aboutPage.emailLabel")}</span>
<span> modstagram.com@gmail.com</span>

View File

@@ -8,6 +8,7 @@ import MainBillboardCard from "@/components/billboards/MainBillboardCard/MainBil
import RoundedDiv from "@/components/elements/RoundedDiv";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { getBillboardDisplayTypeLabel } from "@/lib/billboards/displayTypeLabel";
import { useRouter } from "next/navigation";
import { useTranslation } from "react-i18next";
@@ -84,6 +85,7 @@ function BillboardPayment({ params }: IBillboardProps) {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="font-bold md:text-xl text-lg text-center mt-2 line-clamp-2 mb-4 text-text-blue-light dark:text-text-blue-dark">
{t("billboards.payment.title")}
</h6>

View File

@@ -8,6 +8,7 @@ import MainBillboardCard from "@/components/billboards/MainBillboardCard/MainBil
import RoundedDiv from "@/components/elements/RoundedDiv";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { getBillboardDisplayTypeLabel } from "@/lib/billboards/displayTypeLabel";
import { useRouter, useSearchParams } from "next/navigation";
import Image from "next/image";
@@ -107,6 +108,7 @@ function FailedBillboard() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="font-bold md:text-xl text-lg text-center mt-2 line-clamp-2 mb-4 text-[#FF0000] ">
{t("billboards.payment.failed")}
</h6>

View File

@@ -8,6 +8,7 @@ import MainBillboardCard from "@/components/billboards/MainBillboardCard/MainBil
import RoundedDiv from "@/components/elements/RoundedDiv";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { getBillboardDisplayTypeLabel } from "@/lib/billboards/displayTypeLabel";
import { useSearchParams } from "next/navigation";
import Image from "next/image";
@@ -58,6 +59,7 @@ function SuccessBillboard() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="font-bold md:text-xl text-lg text-center mt-2 line-clamp-2 mb-4 text-[#17A600] ">
{t("billboards.payment.success")}
</h6>

View File

@@ -23,7 +23,7 @@ export default function GlobalError({
return (
<html lang={lang} dir={dir}>
<body className="flex min-h-screen flex-col items-center justify-center gap-4 bg-white px-6 text-center text-neutral-900">
<body className="flex min-h-dvh flex-col items-center justify-center gap-4 bg-white px-6 text-center text-neutral-900">
<p className="text-lg font-bold">{t("errors.appLoad")}</p>
<p className="max-w-md text-sm text-neutral-600">{t("errors.safariIosHint")}</p>
<button

View File

@@ -6,7 +6,7 @@ export default async function Loading() {
const lang = await getServerLanguage();
return (
<div className="flex h-screen w-full flex-col items-center justify-center gap-3 bg-background">
<div className="flex h-dvh w-full flex-col items-center justify-center gap-3 bg-background">
<IOSSpinner size={32} color="#007aff" />
<span className="text-sm text-neutral-500">{translateCommon(lang, "common.loading")}</span>
</div>

View File

@@ -34,13 +34,17 @@ import {
} from "@/lib/postLocationNotice";
import { buildStorageUrl } from "@/components/main/BaseUrl";
import { fetchPostById } from "@/api/fetchPostById";
import SelectBox from "@/components/elements/SelectBox";
import CountryProvinceCitySelect, {
type GeoSelection,
} from "@/components/ui/CountryProvinceCitySelect";
import BackButton from "@/components/ui/BackButton";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import Cookies from "js-cookie";
import { useRouter, useSearchParams } from "next/navigation";
import React, { useEffect, useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
import { ICity, IProvince, User } from "@/types/types";
import { User } from "@/types/types";
type CreateMode = "image" | "video" | "story";
@@ -95,29 +99,20 @@ export default function NewPostPage() {
const [expertiseModalOpen, setExpertiseModalOpen] = useState(false);
const [loadingEdit, setLoadingEdit] = useState(false);
// شهر/استان اختیاری پست — مستقل از موقعیت پروفایل کاربر، مثل بیلبورد/پروژه
const [provinces, setProvinces] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [provinceId, setProvinceId] = useState("");
const [cityId, setCityId] = useState("");
// شهر/استان/کشور اختیاری پست — مستقل از موقعیت پروفایل کاربر، مثل بیلبورد/پروژه
const [postGeoSelection, setPostGeoSelection] = useState<GeoSelection>({
countryId: null,
provinceId: null,
cityId: null,
});
const defaultCountryId = useDefaultCountryId();
useEffect(() => {
request<{ provinces: IProvince[] }>("GET", "/provinces")
.then((res) => setProvinces(res?.provinces || null))
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!provinceId) {
setCities(null);
return;
}
request<{ cities: ICity[] }>("GET", `/cities/${provinceId}`)
.then((res) => setCities(res?.cities || []))
.catch(() => {});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [provinceId]);
if (isEditMode || !defaultCountryId) return;
setPostGeoSelection((prev) =>
prev.countryId ? prev : { ...prev, countryId: defaultCountryId }
);
}, [isEditMode, defaultCountryId]);
const modeLabels: Record<CreateMode, string> = useMemo(
() => ({
@@ -472,8 +467,9 @@ export default function NewPostPage() {
linked_shop_id: selectedShops[0]?._id ?? null,
linked_shop_product_id: selectedShopProducts[0]?._id ?? null,
linked_booking_id: selectedBooking[0]?._id ?? null,
province_id: provinceId || null,
city_id: cityId || null,
country_id: postGeoSelection.countryId || null,
province_id: postGeoSelection.provinceId || null,
city_id: postGeoSelection.cityId || null,
}, { noToast: true });
toast.success(t("posts.postPublished"), { id: "post-upload" });
router.push("/");
@@ -537,14 +533,7 @@ export default function NewPostPage() {
<div className="mx-auto flex min-h-[calc(100dvh-8rem)] max-w-lg flex-col bg-background">
{/* Header — Instagram style */}
<header className="sticky top-0 z-10 flex items-center justify-between border-b border-neutral-200 px-4 py-3 dark:border-neutral-800">
<button
type="button"
onClick={() => router.back()}
className="flex h-9 w-9 items-center justify-center"
aria-label={t("common.back")}
>
<BoldIcon name="arrow-right-3" size={22} className="block dark:invert" />
</button>
<BackButton />
<span className="text-base font-semibold">
{isEditMode
? t("posts.editPostTitle")
@@ -716,33 +705,11 @@ export default function NewPostPage() {
onBookingChange={setSelectedBooking}
/>
<div className="mt-4 flex items-center gap-2">
<SelectBox
value={provinceId}
onChange={(e) => {
setProvinceId(e.target.value);
setCityId("");
}}
>
<option value="">{t("posts.provinceOptional")}</option>
{provinces?.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</SelectBox>
<SelectBox
value={cityId}
onChange={(e) => setCityId(e.target.value)}
disabled={!provinceId}
>
<option value="">{t("posts.cityOptional")}</option>
{cities?.map((item) => (
<option key={item.id} value={item.id}>
{item.name}
</option>
))}
</SelectBox>
<div className="mt-4">
<CountryProvinceCitySelect
value={postGeoSelection}
onChange={setPostGeoSelection}
/>
</div>
</div>
)}

View File

@@ -6,6 +6,7 @@ import {
SidebarInset,
SidebarProvider,
} from "@/components/ui/sidebar"
import BackButton from "@/components/ui/BackButton"
export default function Page() {
return (
@@ -19,9 +20,12 @@ export default function Page() {
>
<SidebarInset>
<div className="flex flex-1 flex-col">
<div className="@container/main flex flex-1 flex-col gap-2">
<div className="px-4 pt-4 lg:px-6">
<BackButton />
</div>
<div className="flex flex-col gap-4 py-4 md:gap-6 md:py-6">
<SectionCards />
<div className="px-4 lg:px-6">

View File

@@ -3,7 +3,7 @@ import { CourseManager } from "@/components/course/CourseManager";
export default function CoursesPage() {
return (
<div className="min-h-screen bg-background">
<div className="min-h-dvh bg-background">
<CourseManager />
</div>
);

View File

@@ -11,6 +11,7 @@ import Image from "next/image";
import toast from "react-hot-toast";
import { motion, AnimatePresence } from "framer-motion";
import { useTranslation } from "react-i18next";
import BackButton from "@/components/ui/BackButton";
interface PaginationData {
page: number;
@@ -86,7 +87,7 @@ export default function PurchasedCoursesPage() {
if (isLoading) {
return (
<LocalePageShell>
<div className="min-h-screen container mx-auto px-4 py-8 lg:px-6">
<div className="min-h-dvh container mx-auto px-4 py-8 lg:px-6">
<AcademyListSkeleton count={4} />
</div>
</LocalePageShell>
@@ -95,10 +96,11 @@ export default function PurchasedCoursesPage() {
return (
<LocalePageShell>
<div className="min-h-screen bg-white dark:bg-neutral-950">
<div className="min-h-dvh bg-white dark:bg-neutral-950">
<div className="relative bg-gradient-to-r from-purple-400 via-purple-600 to-pink-600 dark:from-purple-600 dark:via-purple-800 dark:to-pink-800">
<div className="absolute inset-0 bg-black/10"></div>
<div className="relative container mx-auto px-4 py-12 lg:px-6 md:py-16">
<BackButton tinted className="absolute left-4 top-4 text-white sm:left-6 sm:top-6" />
<motion.div
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}

View File

@@ -8,6 +8,7 @@ import * as z from "zod";
import { motion } from "framer-motion";
import { toast } from "react-hot-toast";
import { useTranslation } from "react-i18next";
import BackButton from "@/components/ui/BackButton";
import {
Form,
@@ -261,6 +262,7 @@ export default function StoreSettings() {
className="w-full max-w-4xl mx-auto px-4 pt-6 lg:px-6 pb-[calc(6rem+env(safe-area-inset-bottom))]"
dir={t("dir") || "rtl"}
>
<BackButton className="mb-4" />
<h2 className="text-3xl font-bold bg-gradient-to-r from-primary to-primary/60 bg-clip-text text-transparent mb-6">
{t("settings.academy.storeProfile.title")}
</h2>

View File

@@ -36,6 +36,7 @@ import { useTranslation } from "react-i18next";
import useAxios from "@/hooks/useAxios";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { useAppLanguage } from "@/contexts/LanguageProvider";
import EditPageHeader from "@/components/settings/EditPageHeader";
interface Payment {
_id: string;
@@ -228,8 +229,9 @@ const WalletDashboard = () => {
return (
<LocalePageShell>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<div className="min-h-screen text-foreground">
<div className="min-h-dvh text-foreground">
<div className="container mx-auto space-y-8 px-4 py-6 lg:px-6">
<EditPageHeader title={t("settings.nav.wallet")} className="my-0" />
{/* کارت‌های آمار - 3 کارت */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">

View File

@@ -5,6 +5,7 @@ import AuthPageLayout, {
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import BackButton from "@/components/ui/BackButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import useAxios from "@/hooks/useAxios";
@@ -74,6 +75,9 @@ function BookingNamePage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center pb-28"

View File

@@ -5,6 +5,7 @@ import AuthPageLayout, {
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import BackButton from "@/components/ui/BackButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import useAxios from "@/hooks/useAxios";
import {
@@ -58,6 +59,9 @@ function BookingPreviewPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<div className="flex w-full flex-1 flex-col items-center pb-28">
<AuthPageContent>
<span className="mb-4 text-xl font-bold">

View File

@@ -5,6 +5,7 @@ import AuthPageLayout, {
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import BackButton from "@/components/ui/BackButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import TimeSelect from "@/components/shops/TimeSelect";
import ToggleSwitch from "@/components/shops/ToggleSwitch";
@@ -160,6 +161,9 @@ function BookingSchedulePage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<div className="flex w-full flex-1 flex-col items-center overflow-y-auto pb-28">
<AuthPageContent>
<span className="mb-4 text-xl font-bold">

View File

@@ -5,6 +5,7 @@ import AuthPageLayout, {
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import AuthNextButton from "@/components/auth/AuthNextButton";
import BackButton from "@/components/ui/BackButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import ServiceItem from "@/components/main/Services/ServiceItem";
import AddServiceModal from "@/components/main/Services/AddServiceModal";
@@ -126,6 +127,9 @@ function BookingServicesPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<div className="flex w-full flex-1 flex-col items-center pb-28">
<AuthPageContent>
<div className="mb-4 flex w-full max-w-sm items-center justify-between">

View File

@@ -16,37 +16,40 @@ function BookingSettingsPage() {
return (
<LocalePageShell>
<Container>
<div className="flex items-center justify-between">
<PageTitle>{t("settings.nav.booking")}</PageTitle>
<div className="flex shrink-0 items-center justify-end gap-3">
<Link
href="/settings/booking/list"
aria-label={t("booking.myListingsSwitcher")}
className="p-2"
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/archive-book.svg")}
className="dark:brightness-0 dark:invert"
/>
</Link>
<Link
href="/settings/booking/new/name"
aria-label={t("booking.addNew")}
className="p-2"
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/add.svg")}
className="dark:brightness-0 dark:invert"
/>
</Link>
</div>
</div>
<PageTitle
action={
<div className="flex items-center gap-3">
<Link
href="/settings/booking/list"
aria-label={t("booking.myListingsSwitcher")}
className="p-2"
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/archive-book.svg")}
className="dark:brightness-0 dark:invert"
/>
</Link>
<Link
href="/settings/booking/new/name"
aria-label={t("booking.addNew")}
className="p-2"
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/add.svg")}
className="dark:brightness-0 dark:invert"
/>
</Link>
</div>
}
>
{t("settings.nav.booking")}
</PageTitle>
<div className="text-xs md:text-sm">
<UserDetails />
<IncomingBookingsList />

View File

@@ -7,6 +7,7 @@ import ProfileAvatar from "@/components/main/ProfileAvatar";
import VerificationBadge from "@/components/main/VerificationBadge";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import RoundedButton from "@/components/elements/RoundedButton";
import BackButton from "@/components/ui/BackButton";
import IOSSpinner from "@/components/ui/IOSSpinner";
import useAxios from "@/hooks/useAxios";
import { useUser } from "@/hooks/useUser";
@@ -153,13 +154,10 @@ export default function NearbyFriendsPage() {
<LocalePageShell>
<Container className="pb-28">
<div className="mb-4 flex items-center gap-3 pt-2">
<Link
<BackButton
href="/settings/chats"
className="flex h-9 w-9 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-800"
aria-label={t("nearbyFriends.back")}
>
<span className="text-lg leading-none"></span>
</Link>
className="bg-neutral-100 dark:bg-neutral-800"
/>
<h1 className="text-base font-bold md:text-lg">
{t("nearbyFriends.title")}
</h1>
@@ -309,7 +307,7 @@ export default function NearbyFriendsPage() {
{t("nearbyFriends.locationOffDesc")}
</p>
<Link href="/settings/profile/user-settings" className="mt-4 block">
<RoundedButton variant="primary" className="w-full max-w-none">
<RoundedButton className="!border-transparent h-9 w-full max-w-none !bg-sky-100 text-sky-600">
{t("nearbyFriends.openSettings")}
</RoundedButton>
</Link>

View File

@@ -4,6 +4,7 @@ import React, { useCallback, useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import Container from "@/components/elements/Container";
import BackButton from "@/components/ui/BackButton";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import VerificationBadge from "@/components/main/VerificationBadge";
import useAxios from "@/hooks/useAxios";
@@ -158,6 +159,7 @@ function RoomMessageRow({
const swipeDragStart =
"onDragStart" in dragProps ? dragProps.onDragStart : undefined;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onDragStart: _, ...restDragProps } = dragProps;
return (
@@ -774,14 +776,12 @@ export default function ChatRoomThreadPage({
className="fixed left-0 right-0 top-0 z-40 border-b border-white/10 bg-black/20 px-3 pb-3 pt-[calc(0.75rem+env(safe-area-inset-top))] backdrop-blur-md"
>
<div className="mx-auto flex w-full max-w-lg items-center gap-3">
<Link
<BackButton
href="/settings/chats/rooms"
className="gentle-transition flex h-10 w-10 items-center justify-center rounded-full bg-white/10 active:scale-95"
aria-label={t("chats.actions.back")}
>
<BoldIcon name="arrow-right-2" size={20} tinted className="text-white" />
</Link>
<div className="min-w-0 flex flex-1 items-center gap-3">
tinted
className="bg-white/10 text-white"
/>
<div className="flex min-w-0 flex-1 items-center justify-center gap-3">
<ProfileAvatar
src={room?.image}
alt={room?.title ?? t("chats.chatRoom")}
@@ -789,32 +789,36 @@ export default function ChatRoomThreadPage({
rounded="full"
className="h-10 w-10 shrink-0"
/>
<div className="min-w-0 flex-1">
<h1 className="truncate text-base font-bold text-white">
{room?.title ?? t("chats.chatRoom")}
</h1>
<div className="min-w-0 max-w-[65%]">
<div className="flex items-center justify-between gap-2">
<h1 className="truncate text-base font-bold text-white">
{room?.title ?? t("chats.chatRoom")}
</h1>
{room?.onlineCount != null ? (
<span className="shrink-0 text-[10px] text-[#22c55e]">
{t("chats.onlineCount", { count: room.onlineCount })}
</span>
) : null}
</div>
{room?.description ? (
<p className="truncate text-[11px] text-white/70">
<p className="truncate text-center text-[11px] text-white/70">
{room.description}
</p>
) : null}
{room?.onlineCount != null ? (
<p className="text-[10px] text-[#22c55e]">
{t("chats.onlineCount", { count: room.onlineCount })}
</p>
) : null}
</div>
</div>
{canAddMembers ? (
<button
type="button"
onClick={() => setShowAddMembers(true)}
className="gentle-transition flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-white/10 active:scale-95"
className="gentle-transition flex h-9 w-9 shrink-0 items-center justify-center rounded-full bg-white/10 active:scale-95"
aria-label={t("chats.aria.addUser")}
>
<BoldIcon name="user-add" size={20} tinted className="text-white" />
</button>
) : null}
) : (
<span className="h-9 w-9 shrink-0" />
)}
</div>
{room?.members?.length ? (
<div className="mx-auto mt-3 flex w-full max-w-lg gap-2 overflow-x-auto pb-1">

View File

@@ -9,6 +9,7 @@ import Link from "next/link";
import { useCallback, useEffect, useState } from "react";
import IOSSpinner from "@/components/ui/IOSSpinner";
import BoldIcon from "@/components/ui/BoldIcon";
import BackButton from "@/components/ui/BackButton";
import { usePageTitle } from "@/hooks/usePageTitle";
import {
acquireChatSocket,
@@ -86,15 +87,18 @@ export default function ChatRoomsPage() {
return (
<LocalePageShell>
<Container>
<div className="flex items-center justify-between gap-3 px-4 py-7">
<h1 className="text-2xl font-bold text-foreground">{t("chats.chatRoom")}</h1>
<div className="grid grid-cols-[2.5rem_1fr_2.5rem] items-center gap-3 px-4 py-7">
<BackButton className="justify-self-start" />
<h1 className="truncate text-center text-2xl font-bold text-foreground">
{t("chats.chatRoom")}
</h1>
<button
type="button"
onClick={() => setShowCreateModal(true)}
className="gentle-transition flex h-11 w-11 shrink-0 items-center justify-center rounded-full bg-[#0095f6] text-white shadow-md active:scale-95"
className="gentle-transition flex h-9 w-9 shrink-0 items-center justify-center justify-self-end rounded-full bg-[#0095f6] text-white shadow-md active:scale-95"
aria-label={t("chats.aria.createRoom")}
>
<BoldIcon name="add" size={22} tinted className="text-white" />
<BoldIcon name="add" size={20} tinted className="text-white" />
</button>
</div>

View File

@@ -1,13 +1,14 @@
"use client";
import { useEffect, useRef, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { useParams } from "next/navigation";
import Container from "@/components/elements/Container";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import ChatBoldIcon from "@/components/chat/ChatBoldIcon";
import { chatHeaderCircleBtn, chatHeaderInfoPill, chatHeaderGlassStyle } from "@/components/chat/ChatHeader";
import VideoMessageBubble from "@/components/chat/VideoMessageBubble";
import BackButton from "@/components/ui/BackButton";
import IOSSpinner from "@/components/ui/IOSSpinner";
import useAxios from "@/hooks/useAxios";
import { cn } from "@/lib/utils";
@@ -54,7 +55,6 @@ function getGroupPosition(
export default function ShopChatPage() {
const { t } = useTranslation("common");
const router = useRouter();
const params = useParams<{ shopId: string; buyerId: string }>();
const { shopId, buyerId } = params;
const { request } = useAxios();
@@ -149,15 +149,12 @@ export default function ShopChatPage() {
dir="rtl"
className="pointer-events-auto mx-auto flex max-w-lg items-center gap-2"
>
<button
type="button"
onClick={() => router.back()}
<BackButton
tinted
iconSize={22}
className={chatHeaderCircleBtn}
style={chatHeaderGlassStyle}
aria-label={t("chats.actions.back")}
>
<ChatBoldIcon name="back" size={22} className="text-current" />
</button>
/>
<div dir="ltr" className={chatHeaderInfoPill} style={chatHeaderGlassStyle}>
<div dir="rtl" className="min-w-0 flex-1 text-right">

View File

@@ -9,6 +9,7 @@ import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import AuthUserDetails from "@/components/auth/AuthUserDetails";
import EditPageHeader from "@/components/settings/EditPageHeader";
import Image from "next/image";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import AuthRules from "@/components/auth/AuthRules";
@@ -63,10 +64,8 @@ function NationalCart() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center address-page">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.authentication")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center address-page">
<EditPageHeader title={t("settings.edit.nav.authentication")} />
<div className="mt-5">
<AuthUserDetails />
</div>

View File

@@ -9,12 +9,13 @@ import Image from "next/image";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { useUser } from "@/hooks/useUser";
import { formatFullName } from "@/lib/formatFullName";
import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { BASE_URL } from "@/components/main/BaseUrl";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import Cookies from "js-cookie";
import axios from "axios";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import EditPageHeader from "@/components/settings/EditPageHeader";
import { useTranslation } from "react-i18next";
function LicensePage() {
@@ -181,7 +182,7 @@ function LicensePage() {
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center justify-center address-page">
<span className="text-xl mb-9 font-bold">{t("settings.edit.license.title")}</span>
<EditPageHeader title={t("settings.edit.license.title")} className="mb-9" />
<div className="flex flex-col justify-center items-center">
<ProfileAvatar

View File

@@ -62,7 +62,7 @@ export default function AnalyticsHubPage() {
<div className="flex w-full items-center gap-3 rounded-2xl border border-neutral-200 p-4 text-right dark:border-neutral-700">
<BoldIcon name="global" size={22} tinted />
<span className="flex-1 text-sm font-semibold">
{t("settings.region.title")}
{t("settings.edit.region.title")}
</span>
</div>
</Link>

View File

@@ -43,6 +43,7 @@ type PostAnalyticsResponse = {
};
geography: {
locatedViewsCount: number;
viewsByCountry: { name: string; count: number }[];
viewsByProvince: { name: string; count: number }[];
viewsByCity: { name: string; count: number }[];
};
@@ -163,6 +164,43 @@ export default function PostAnalyticsDetailPage() {
</div>
</div>
<div className="mt-4 rounded-2xl border border-neutral-200 p-4 dark:border-neutral-800">
<h3 className="mb-1 text-sm font-bold">
{t("settings.edit.postsAnalytics.countryTitle")}
</h3>
<p className="mb-3 text-[11px] text-neutral-400">
{t("settings.edit.postsAnalytics.geographyHint", {
count: geography.locatedViewsCount,
})}
</p>
{geography.viewsByCountry.length === 0 ? (
<p className="py-6 text-center text-sm text-neutral-500">
{t("settings.edit.postsAnalytics.countryEmpty")}
</p>
) : (
<div className="h-56 w-full">
<ResponsiveContainer width="100%" height="100%">
<BarChart
data={geography.viewsByCountry.slice(0, 8)}
layout="vertical"
margin={{ top: 5, right: 10, left: 0, bottom: 0 }}
>
<CartesianGrid strokeDasharray="3 3" opacity={0.2} horizontal={false} />
<XAxis type="number" allowDecimals={false} tick={{ fontSize: 10 }} />
<YAxis
type="category"
dataKey="name"
width={70}
tick={{ fontSize: 10 }}
/>
<Tooltip />
<Bar dataKey="count" fill="#387E65" radius={[0, 6, 6, 0]} />
</BarChart>
</ResponsiveContainer>
</div>
)}
</div>
<div className="mt-4 rounded-2xl border border-neutral-200 p-4 dark:border-neutral-800">
<h3 className="mb-1 text-sm font-bold">
{t("settings.edit.postsAnalytics.geographyTitle")}

View File

@@ -53,7 +53,7 @@ export default function RegionPage() {
province_id: selection.provinceId,
city_id: selection.cityId,
});
toast.success(t("settings.region.saveSuccess"));
toast.success(t("settings.edit.region.saveSuccess"));
} catch {
toast.error(t("shops.unknownError"));
}
@@ -64,17 +64,17 @@ export default function RegionPage() {
return (
<LocalePageShell>
<Container>
<PageTitle>{t("settings.region.title")}</PageTitle>
<PageTitle>{t("settings.edit.region.title")}</PageTitle>
<div className="mx-auto mt-8 w-full max-w-sm">
<p className="mb-4 text-center text-xs text-neutral-500">
{t("settings.region.hint")}
{t("settings.edit.region.hint")}
</p>
<CountryProvinceCitySelect value={selection} onChange={setSelection} />
{selectedCountry && (
<p className="mt-3 text-center text-xs text-neutral-400">
{t("settings.region.selectedLabel", {
{t("settings.edit.region.selectedLabel", {
country: selectedCountry.name_fa,
})}
</p>

View File

@@ -4,6 +4,7 @@ import AuthNextButton from "@/components/auth/AuthNextButton";
import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import EditPageHeader from "@/components/settings/EditPageHeader";
import useAxios from "@/hooks/useAxios";
import { useUser } from "@/hooks/useUser";
import Image from "next/image";
@@ -67,11 +68,9 @@ function AvatarPage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.avatar")} />
<div className="flex flex-col items-center mb-4">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.avatar")}
</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 ? (
<>

View File

@@ -9,6 +9,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import { useUser } from "@/hooks/useUser";
import { useTranslation } from "react-i18next";
@@ -56,10 +57,8 @@ function BioPage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.bio")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.bio")} />
<div className="mt-5"></div>
<form
onSubmit={formik.handleSubmit}

View File

@@ -7,6 +7,7 @@ import useAxios from "@/hooks/useAxios";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import Image from "next/image";
import { useUser } from "@/hooks/useUser";
@@ -90,8 +91,8 @@ function Colors() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">{t("settings.edit.nav.colors")}</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.colors")} />
<p className="mt-8 text-center text-sm font-bold">{t("settings.edit.sizes.appearance")}</p>
{/* Buttons for categories */}

View File

@@ -9,6 +9,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import { useUser } from "@/hooks/useUser";
import { useTranslation } from "react-i18next";
@@ -52,10 +53,8 @@ function CooperationType() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.cooperationType")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.cooperationType")} />
<p className="my-10 text-sm font-bold">
{t("settings.edit.cooperationType.question")}
</p>

View File

@@ -16,6 +16,7 @@ import {
subExpertiseListIncludes,
} from "@/lib/subExpertiseDisplay";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import EditPageHeader from "@/components/settings/EditPageHeader";
import { useTranslation } from "react-i18next";
function Expertise() {
@@ -150,10 +151,8 @@ function Expertise() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center min-h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.expertise")}
</span>
<div className="p-4 flex flex-col items-center min-h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.expertise")} />
<p className="mt-8 text-center">{t("settings.edit.expertise.question")}</p>

View File

@@ -1,281 +1,215 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import React, { useEffect, useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import SelectBox from "@/components/elements/SelectBox";
import { ICity, IProvince } from "@/types/types";
import Map, { GeolocateControl, Marker } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import Image from "next/image";
import { useUser } from "@/hooks/useUser";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
function LocationPage() {
const { t } = useTranslation("common");
const schema = useMemo(
() =>
yup.object().shape({
markerCoordinate: yup.mixed().required(t("settings.edit.location.markerRequired")),
address: yup.string().required(t("settings.edit.location.addressRequired")),
cityId: yup.string().required(t("settings.edit.location.cityRequired")),
stateId: yup.string().required(t("settings.edit.location.stateRequired")),
}),
[t]
);
const router = useRouter();
const { request, loading } = useAxios();
const user = useUser();
const [isCheckedOne, setIsCheckedOne] = useState<boolean | null>(false);
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [selectedLocation, setSelectedLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
const fetchStates = async () => {
try {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
);
setAllStates(response?.provinces || null);
// console.log(response?.provinces);
} catch (err) {
console.log(err);
}
};
// Fetch cities when a province is selected
const fetchCities = async (provinceId: string) => {
try {
const response = await request<{ cities: ICity[] }>(
"GET",
`/cities/${provinceId}`
);
setCities(response?.cities || []);
} catch (err) {
console.log(err);
}
};
useEffect(() => {
fetchStates();
}, []);
const toggleCheckBox = () => {
setIsCheckedOne(!isCheckedOne);
};
const formik = useFormik({
initialValues: {
markerCoordinate: [] as number[],
address: "",
cityId: "",
stateId: "",
},
validationSchema: schema,
onSubmit: async (values, { setSubmitting }) => {
try {
await request("PATCH", "/verify/address", {
address: values.address,
province_id: values.stateId,
city_id: values.cityId,
lat: values.markerCoordinate[1],
lng: values.markerCoordinate[0],
show_location: isCheckedOne,
});
router.push("/settings/edit");
} catch (err: any) {
toast.error(err?.message || t("settings.edit.unknownError"));
} finally {
setSubmitting(false);
}
},
});
// Handle province change
const handleProvinceChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedProvinceId = e.target.value;
formik.setFieldValue("stateId", selectedProvinceId);
fetchCities(selectedProvinceId); // Fetch cities for the selected province
};
const handleMapClick = (event: any) => {
const { lngLat } = event;
setSelectedLocation({
lat: lngLat.lat,
lng: lngLat.lng,
});
formik.setFieldValue("markerCoordinate", [lngLat.lng, lngLat.lat]);
};
useEffect(() => {
if (user) {
formik.setValues({
markerCoordinate:
user.lat && user.lng
? [Number(user.lng), Number(user.lat)]
: [],
address: user.address || "",
cityId: user?.city?.id ? String(user.city.id) : "",
stateId: user?.province?.id ? String(user.province.id) : "",
});
if (user.lat && user.lng) {
setSelectedLocation({
lat: Number(user.lat),
lng: Number(user.lng),
});
}
if (user?.province?.id) {
fetchCities(user?.province?.id.toString());
}
if (user?.show_location !== undefined) {
setIsCheckedOne(user?.show_location);
}
}
}, [user]);
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center justify-center address-page ">
<span className="text-xl font-bold text-foreground">{t("settings.edit.nav.location")}</span>
<div className="mt-5"></div>
<small className="font-bold mt-10 mb-4">
{t("settings.edit.location.question")}
</small>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<SelectBox
className={`mt-4 ${
formik.touched.stateId && formik.errors.stateId
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.stateId}
onChange={handleProvinceChange}
>
<option disabled value="">
{t("settings.profile.province")}
</option>
{allStates?.map((item: IProvince) => (
<option key={item.id} value={item.id}>
{item?.name}
</option>
))}
</SelectBox>
{formik.touched.stateId && formik.errors.stateId && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.stateId}
</small>
)}
<SelectBox
className={`mt-4 ${
formik.touched.cityId && formik.errors.cityId
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.cityId}
onChange={(e) => formik.setFieldValue("cityId", e.target.value)}
>
<option disabled value="">
{t("settings.profile.city")}
</option>
{cities?.map((city: ICity) => (
<option key={city.id} value={city.id}>
{city.name}
</option>
))}
</SelectBox>
{formik.touched.cityId && formik.errors.cityId && (
<small className="text-red-500 mt-2 block text-center">
{formik.errors.cityId}
</small>
)}
<textarea
name="address"
placeholder={t("settings.edit.location.addressPlaceholder")}
className={`border mt-4 w-full max-w-[300px] p-3 rounded-2xl border-border-primary-light dark:border-border-primary-dark bg-secondary-light dark:bg-secondary-dark font-medium ${
formik.touched.address && formik.errors.address
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.address && formik.errors.address && (
<small className="text-red-500 mt-2 block">
{formik.errors.address}
</small>
)}
<Map
style={{ height: "calc(100vh - 260px)" }}
initialViewState={{
longitude: Number(user?.lng) || 51.375433528216654,
latitude: Number(user?.lat) || 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{selectedLocation && (
<Marker
latitude={selectedLocation.lat}
longitude={selectedLocation.lng}
>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src={"/images/icons/location.svg"}
/>
</Marker>
)}
</Map>
<label className="flex items-center gap-2">
<input
className="scale-125"
type="checkbox"
onChange={toggleCheckBox}
checked={isCheckedOne || false}
/>
<span className="text-xs font-bold">
{t("settings.edit.location.showToAll")}
</span>
</label>
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
{t("settings.edit.save")}
</AuthNextButton>
</form>
</div>
</Container>
</LocalePageShell>
);
}
export default LocationPage;
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import Container from "@/components/elements/Container";
import React, { useEffect, useState } from "react";
import useAxios from "@/hooks/useAxios";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import toast from "react-hot-toast";
import CountryProvinceCitySelect, {
type GeoSelection,
countryHasProvinces,
} from "@/components/ui/CountryProvinceCitySelect";
import EditPageHeader from "@/components/settings/EditPageHeader";
import Map, { GeolocateControl, Marker } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
import Image from "next/image";
import { useUser } from "@/hooks/useUser";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { useTranslation } from "react-i18next";
import { useMemo } from "react";
function LocationPage() {
const { t } = useTranslation("common");
const schema = useMemo(
() =>
yup.object().shape({
markerCoordinate: yup.mixed().required(t("settings.edit.location.markerRequired")),
address: yup.string().required(t("settings.edit.location.addressRequired")),
}),
[t]
);
const router = useRouter();
const { request, loading } = useAxios();
const user = useUser();
const [isCheckedOne, setIsCheckedOne] = useState<boolean | null>(false);
const [geoSelection, setGeoSelection] = useState<GeoSelection>({
countryId: null,
provinceId: null,
cityId: null,
});
const [selectedLocation, setSelectedLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
const toggleCheckBox = () => {
setIsCheckedOne(!isCheckedOne);
};
const formik = useFormik({
initialValues: {
markerCoordinate: [] as number[],
address: "",
},
validationSchema: schema,
onSubmit: async (values, { setSubmitting }) => {
const provinceRequired = countryHasProvinces(geoSelection.countryId);
if (provinceRequired && !geoSelection.provinceId) {
toast.error(t("settings.edit.location.stateRequired"));
setSubmitting(false);
return;
}
if (provinceRequired && !geoSelection.cityId) {
toast.error(t("settings.edit.location.cityRequired"));
setSubmitting(false);
return;
}
try {
await request("PATCH", "/verify/address", {
address: values.address,
country_id: geoSelection.countryId,
province_id: geoSelection.provinceId,
city_id: geoSelection.cityId,
lat: values.markerCoordinate[1],
lng: values.markerCoordinate[0],
show_location: isCheckedOne,
});
router.push("/settings/edit");
} catch (err: any) {
toast.error(err?.message || t("settings.edit.unknownError"));
} finally {
setSubmitting(false);
}
},
});
const handleMapClick = (event: any) => {
const { lngLat } = event;
setSelectedLocation({
lat: lngLat.lat,
lng: lngLat.lng,
});
formik.setFieldValue("markerCoordinate", [lngLat.lng, lngLat.lat]);
};
useEffect(() => {
if (user) {
formik.setValues({
markerCoordinate:
user.lat && user.lng
? [Number(user.lng), Number(user.lat)]
: [],
address: user.address || "",
});
if (user.lat && user.lng) {
setSelectedLocation({
lat: Number(user.lat),
lng: Number(user.lng),
});
}
setGeoSelection({
countryId: user?.country?.id ?? null,
provinceId: user?.province?.id ?? null,
cityId: user?.city?.id ?? null,
});
if (user?.show_location !== undefined) {
setIsCheckedOne(user?.show_location);
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [user]);
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center justify-center address-page ">
<EditPageHeader title={t("settings.edit.nav.location")} />
<div className="mt-5"></div>
<small className="font-bold mt-10 mb-4">
{t("settings.edit.location.question")}
</small>
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"
>
<CountryProvinceCitySelect
value={geoSelection}
onChange={setGeoSelection}
className="mt-4 w-full"
/>
<textarea
name="address"
placeholder={t("settings.edit.location.addressPlaceholder")}
className={`border mt-4 w-full max-w-[300px] p-3 rounded-2xl border-border-primary-light dark:border-border-primary-dark bg-secondary-light dark:bg-secondary-dark font-medium ${
formik.touched.address && formik.errors.address
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.address}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
/>
{formik.touched.address && formik.errors.address && (
<small className="text-red-500 mt-2 block">
{formik.errors.address}
</small>
)}
<Map
style={{ height: "calc(100dvh - 260px)" }}
initialViewState={{
longitude: Number(user?.lng) || 51.375433528216654,
latitude: Number(user?.lat) || 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{selectedLocation && (
<Marker
latitude={selectedLocation.lat}
longitude={selectedLocation.lng}
>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src={"/images/icons/location.svg"}
/>
</Marker>
)}
</Map>
<label className="flex items-center gap-2">
<input
className="scale-125"
type="checkbox"
onChange={toggleCheckBox}
checked={isCheckedOne || false}
/>
<span className="text-xs font-bold">
{t("settings.edit.location.showToAll")}
</span>
</label>
<AuthNextButton type="submit" className="mt-20" loading={loading} disabled={loading}>
{t("settings.edit.save")}
</AuthNextButton>
</form>
</div>
</Container>
</LocalePageShell>
);
}
export default LocationPage;

View File

@@ -12,6 +12,7 @@ import * as yup from "yup";
import { useRouter } from "next/navigation";
import { IVerifyOtp } from "@/types/types";
import AuthNextButton from "@/components/auth/AuthNextButton";
import BackButton from "@/components/ui/BackButton";
import { useUser } from "@/hooks/useUser";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
@@ -74,7 +75,10 @@ function PasswordPage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<div className="w-full max-w-sm">
<BackButton />
</div>
<AuthHead
title={
isGoogleUser

View File

@@ -9,6 +9,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import { useUser } from "@/hooks/useUser";
import { useTranslation } from "react-i18next";
@@ -58,10 +59,8 @@ function PublicRelations() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.publicRelations")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.publicRelations")} />
<form onSubmit={formik.handleSubmit} className="w-full max-w-sm flex flex-col items-center">
<small className="font-bold mt-10">{t("settings.edit.bio.aboutYou")}</small>
<textarea

View File

@@ -8,6 +8,7 @@ import AuthUserDetails from "@/components/auth/AuthUserDetails";
import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import useAxios from "@/hooks/useAxios";
import toast from "react-hot-toast";
import { dataURLtoBlob } from "@/helpers/helpers";
@@ -78,10 +79,8 @@ const ServicesPage: React.FC = () => {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.services")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.services")} />
<div className="mt-5 w-full max-w-md">
<AuthUserDetails />
</div>

View File

@@ -9,6 +9,7 @@ import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import AuthInput from "@/components/auth/AuthInput";
import DatePicker from "react-multi-date-picker";
@@ -128,10 +129,8 @@ function ShabaPage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center min-h-screen justify-center auth-page">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.shaba")}
</span>
<div className="p-4 flex flex-col items-center min-h-dvh justify-center auth-page">
<EditPageHeader title={t("settings.edit.nav.shaba")} />
<form
onSubmit={formik.handleSubmit}
className="w-full max-w-sm flex flex-col items-center"

View File

@@ -8,6 +8,7 @@ import useAxios from "@/hooks/useAxios";
import * as yup from "yup";
import { useRouter } from "next/navigation";
import AuthNextButton from "@/components/auth/AuthNextButton";
import EditPageHeader from "@/components/settings/EditPageHeader";
import toast from "react-hot-toast";
import Image from "next/image";
import { useUser } from "@/hooks/useUser";
@@ -65,10 +66,8 @@ function Sizes() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<span className="text-xl font-bold text-foreground">
{t("settings.edit.nav.sizes")}
</span>
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<EditPageHeader title={t("settings.edit.nav.sizes")} />
<p className="mt-8 text-center text-sm font-bold mb-4">
{t("settings.edit.sizes.appearance")}
</p>

View File

@@ -10,6 +10,7 @@ import AuthInput from "@/components/auth/AuthInput";
import AuthNextButton from "@/components/auth/AuthNextButton";
import Container from "@/components/elements/Container";
import UsernameSuggestions from "@/components/auth/UsernameSuggestions";
import BackButton from "@/components/ui/BackButton";
import { usernameFormSchema } from "@/lib/validation/usernameSchema";
import { sanitizeUsernameInput, USERNAME_MIN_LENGTH, USERNAME_MAX_LENGTH } from "@/lib/validation/username";
import LocalePageShell from "@/components/i18n/LocalePageShell";
@@ -67,7 +68,10 @@ function UsernamePage() {
return (
<LocalePageShell>
<Container>
<div className="p-4 flex flex-col items-center h-screen justify-center">
<div className="p-4 flex flex-col items-center h-dvh justify-center">
<div className="w-full max-w-sm">
<BackButton />
</div>
<AuthHead title={t("settings.edit.nav.username")} />
<form
onSubmit={formik.handleSubmit}

View File

@@ -9,6 +9,7 @@ import MainBillboardCardActions from "@/components/billboards/MainBillboardCard/
import BillboardDetails from "@/components/billboards/BillboardPage/BillboardDetails";
import MyBillboardStatusActions from "@/components/settings/my-billboards/MyBillboardStatusActions";
import BillboardLocationLabels from "@/components/settings/my-billboards/BillboardLocationLabels";
import BackButton from "@/components/ui/BackButton";
interface IBillboardProps {
params: Promise<{ id: string; title: string }>;
@@ -32,6 +33,7 @@ async function BillboardPage({ params }: IBillboardProps) {
return (
<Container>
<BackButton />
<a
href={`/billboards/profile/${billboard?.creatorId}/${billboard?.title}`}
>

View File

@@ -4,7 +4,9 @@
import React, { useEffect, useState, useCallback, useMemo } from "react";
import dynamic from "next/dynamic";
import Container from "@/components/elements/Container";
import EditPageHeader from "@/components/settings/EditPageHeader";
import useAxios from "@/hooks/useAxios";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import { useFormik } from "formik";
import * as yup from "yup";
import { useRouter } from "next/navigation";
@@ -81,6 +83,7 @@ function EditAdProfile({ params }: IEditProps) {
[t]
);
const defaultCountryId = useDefaultCountryId();
const [avatar, setAvatar] = useState<string | null>(null);
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
@@ -124,14 +127,14 @@ function EditAdProfile({ params }: IEditProps) {
try {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
defaultCountryId ? `/provinces?country_id=${defaultCountryId}` : "/provinces"
);
setAllStates(response?.provinces ?? null);
} catch (err) {
console.error("Error fetching provinces:", err);
toast.error(t("settings.billboards.editProfile.loadProvincesError"));
}
}, [request, t]);
}, [request, t, defaultCountryId]);
// دریافت شهرها
const fetchCities = useCallback(
@@ -346,7 +349,7 @@ function EditAdProfile({ params }: IEditProps) {
className="p-4 flex flex-col items-center justify-center address-page"
aria-label={t("settings.billboards.editProfile.pageAria")}
>
<span className="text-xl font-bold">{t("settings.billboards.editProfile.title")}</span>
<EditPageHeader title={t("settings.billboards.editProfile.title")} />
<div className="mt-5" />
<form
onSubmit={formik.handleSubmit}
@@ -370,7 +373,7 @@ function EditAdProfile({ params }: IEditProps) {
onProvinceChange={handleProvinceChange}
/>
<Map
style={{ height: "calc(100vh - 260px)" }}
style={{ height: "calc(100dvh - 260px)" }}
initialViewState={mapInitialViewState}
aria-label={t("settings.billboards.editProfile.mapAria")}
reuseMaps // بهینه‌سازی برای جلوگیری از re-mounting مکرر

View File

@@ -26,7 +26,7 @@ export default function ProfileClient() {
if (loading || !user) {
return (
<div className="flex h-screen items-center justify-center" />
<div className="flex h-dvh items-center justify-center" />
);
}

View File

@@ -0,0 +1,129 @@
"use client";
import Container from "@/components/elements/Container";
import PageTitle from "@/components/settings/PageTitle";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import RoundedInput from "@/components/elements/RoundedInput";
import BoldIcon from "@/components/ui/BoldIcon";
import { useAppLanguage } from "@/contexts/LanguageProvider";
import {
SUPPORTED_LANGUAGES,
LANGUAGE_REGISTRY,
type AppLanguage,
} from "@/lib/i18n/registry";
import useAxios from "@/hooks/useAxios";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
export default function SelectLanguagePage() {
const { t } = useTranslation("common");
const { language, setLanguage } = useAppLanguage();
const { request } = useAxios();
const router = useRouter();
const [query, setQuery] = useState("");
const languages = useMemo(() => {
const q = query.trim().toLowerCase();
return SUPPORTED_LANGUAGES.map((code) => LANGUAGE_REGISTRY[code]).filter(
(def) =>
!q ||
def.nativeLabel.toLowerCase().includes(q) ||
def.englishLabel.toLowerCase().includes(q)
);
}, [query]);
const handleSelect = async (code: AppLanguage) => {
const def = LANGUAGE_REGISTRY[code];
if (!def.enabled) {
toast(t("language.comingSoon"));
return;
}
if (code === language) {
router.back();
return;
}
try {
await setLanguage(code);
try {
await request(
"PATCH",
"/account/privacy",
{ preferred_language: code },
{ noToast: true }
);
} catch {
// local language still applies when offline / old backend
}
toast.success(t("language.saved"));
router.back();
} catch {
toast.error(t("common.saveError"));
}
};
return (
<LocalePageShell>
<Container>
<PageTitle>{t("language.selectTitle")}</PageTitle>
<div className="mx-auto w-full max-w-md">
<RoundedInput
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("language.searchPlaceholder")}
className="mb-4"
/>
{languages.length === 0 ? (
<p className="py-10 text-center text-sm text-neutral-500">
{t("language.noResults")}
</p>
) : (
<div className="flex flex-col gap-2 pb-8">
{languages.map((def) => {
const active = def.code === language;
return (
<button
key={def.code}
type="button"
dir={def.direction}
onClick={() => void handleSelect(def.code as AppLanguage)}
className={`flex items-center justify-between rounded-xl border px-4 py-3 text-right transition-colors ${
active
? "border-[#0095f6] bg-[#0095f6]/10"
: "border-neutral-200 dark:border-neutral-800"
} ${!def.enabled ? "opacity-60" : ""}`}
>
<div className="flex flex-col items-start">
<span className="text-sm font-semibold">
{def.nativeLabel}
</span>
<span className="text-xs text-neutral-500">
{def.englishLabel}
</span>
</div>
{active ? (
<BoldIcon
name="check"
size={18}
tinted
className="text-[#0095f6]"
/>
) : !def.enabled ? (
<span className="rounded-full bg-neutral-100 px-2 py-0.5 text-[10px] font-semibold text-neutral-500 dark:bg-neutral-800">
{t("language.comingSoonBadge")}
</span>
) : null}
</button>
);
})}
</div>
)}
</div>
</Container>
</LocalePageShell>
);
}

View File

@@ -159,6 +159,14 @@ export default function UserSettingsPage() {
<LanguageSettingRow />
<Link
href="/settings/edit/analytics/region"
className="flex items-center justify-between rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800"
>
<span className="font-semibold">{t("settings.edit.region.title")}</span>
<span className="text-neutral-400"></span>
</Link>
<Link
href="/settings/profile/visitors"
className="flex items-center justify-between rounded-xl border border-neutral-200 px-4 py-3 dark:border-neutral-800"

View File

@@ -90,8 +90,6 @@ export default function ShopSettingsPage() {
router.push(`/shops/${myShops[0]._id}/products/new/name`);
};
const primaryShopId = myShops && myShops.length > 0 ? myShops[0]._id : null;
const { data: sellerOrdersData, isLoading: sellerOrdersLoading } = useInfiniteScroll({
endpoint: "/orders",
queryKey: ["seller-orders", search],
@@ -119,38 +117,38 @@ export default function ShopSettingsPage() {
<Header />
<LocalePageShell>
<Container>
<div className="flex items-center justify-between">
<span className="w-20 shrink-0" />
<PageTitle>{t("settings.nav.shop")}</PageTitle>
{filter === FILTER_SELLER ? (
<div className="flex w-20 shrink-0 items-center justify-end gap-3">
<button
type="button"
onClick={() => router.push("/settings/shop/list")}
aria-label={t("shops.mySwitcher")}
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/shop-add.svg")}
className="dark:invert"
/>
</button>
<button type="button" onClick={handleAddShop} aria-label={t("shops.addShop")}>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/add.svg")}
className="dark:invert"
/>
</button>
</div>
) : (
<span className="w-20 shrink-0" />
)}
</div>
<PageTitle
action={
filter === FILTER_SELLER ? (
<div className="flex items-center gap-3">
<button
type="button"
onClick={() => router.push("/settings/shop/list")}
aria-label={t("shops.mySwitcher")}
>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/shop-add.svg")}
className="dark:invert"
/>
</button>
<button type="button" onClick={handleAddShop} aria-label={t("shops.addShop")}>
<Image
width={24}
height={24}
alt=""
src={staticIconUrl("/images/icons/add.svg")}
className="dark:invert"
/>
</button>
</div>
) : undefined
}
>
{t("settings.nav.shop")}
</PageTitle>
<div className="text-xs md:text-sm">
<UserDetails />

View File

@@ -7,6 +7,7 @@ import Header from "@/components/main/Header";
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
import ShareShopModal from "@/components/shops/ShareShopModal";
import ShopProductListRow from "@/components/shops/ShopProductListRow";
import PageTitle from "@/components/settings/PageTitle";
import BoldIcon from "@/components/ui/BoldIcon";
import TabNavigation from "@/components/TabNavigation";
import useAxios from "@/hooks/useAxios";
@@ -111,9 +112,7 @@ export default function ShopProductsPage() {
<span className="truncate text-sm font-semibold">{shop?.name}</span>
</div>
<h1 className="my-6 text-center text-2xl font-bold">
{t("shops.myProductsTitle")}
</h1>
<PageTitle>{t("shops.myProductsTitle")}</PageTitle>
<div className="mx-auto w-full max-w-md">
<div className="flex items-center gap-2">

View File

@@ -5,6 +5,8 @@ import LocalePageShell from "@/components/i18n/LocalePageShell";
import Header from "@/components/main/Header";
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
import BoldIcon from "@/components/ui/BoldIcon";
import BackButton from "@/components/ui/BackButton";
import DiscountBadge from "@/components/ui/DiscountBadge";
import TabNavigation from "@/components/TabNavigation";
import useAxios from "@/hooks/useAxios";
import { saveProductWizardId } from "@/hooks/useProductWizardId";
@@ -211,7 +213,8 @@ export default function ProductManagePage() {
<Header />
<LocalePageShell>
<Container className="pb-28">
<div className="flex items-center justify-end gap-2">
<div className="flex items-center justify-between gap-2">
<BackButton />
<span className="text-sm font-bold">{shopInfo?.name}</span>
<span className="flex h-9 w-9 shrink-0 items-center justify-center overflow-hidden rounded-full bg-neutral-100 dark:bg-neutral-800">
{shopInfo?.logo ? (
@@ -235,11 +238,7 @@ export default function ProductManagePage() {
<div className="mx-auto mt-4 flex w-full max-w-md flex-col gap-4 rounded-3xl border border-neutral-200 p-4 dark:border-neutral-700">
<div className="flex items-center justify-center gap-2">
{discountPercent != null && (
<span className="rounded-full bg-red-50 px-2 py-0.5 text-xs font-bold text-red-500 dark:bg-red-500/10">
{t("shops.discountPercentLabel", { percent: discountPercent })}
</span>
)}
{discountPercent != null && <DiscountBadge percent={discountPercent} />}
<h1 className="text-xl font-bold">{listing.title}</h1>
</div>

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import ProductCategoryPicker from "@/components/shops/ProductCategoryPicker";
@@ -107,6 +108,9 @@ function ProductCategoryPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold">{t("shops.productCategoryTitle")}</span>
<ProductCategoryPicker

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
@@ -99,6 +100,9 @@ function ProductImagesPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.productImagesTitle")}</span>
<p className="mb-4 text-xs text-neutral-500">

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
@@ -136,6 +137,9 @@ function ProductNamePage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.productNameTitle")}</span>

View File

@@ -4,9 +4,11 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import DiscountBadge from "@/components/ui/DiscountBadge";
import useAxios from "@/hooks/useAxios";
import {
clearProductWizardId,
@@ -180,6 +182,9 @@ function ProductVariantsPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.variantsTitle")}</span>
@@ -271,8 +276,8 @@ function ProductVariantsPage() {
/>
</div>
{percent != null && (
<span className="mt-1 inline-block rounded-md bg-red-50 px-1.5 py-0.5 text-[10px] font-bold text-red-600 dark:bg-red-500/10">
{t("shops.discountPercentLabel", { percent })}
<span className="mt-1 inline-block">
<DiscountBadge percent={percent} />
</span>
)}
</div>

View File

@@ -6,6 +6,8 @@ import Header from "@/components/main/Header";
import { IMAGE_BASE_URL, staticIconUrl } from "@/components/main/BaseUrl";
import ProductImageLightbox from "@/components/shops/ProductImageLightbox";
import BoldIcon from "@/components/ui/BoldIcon";
import BackButton from "@/components/ui/BackButton";
import DiscountBadge from "@/components/ui/DiscountBadge";
import TabNavigation from "@/components/TabNavigation";
import useAxios from "@/hooks/useAxios";
import { cn } from "@/lib/utils";
@@ -251,6 +253,9 @@ export default function ListingDetailClient() {
<Header />
<LocalePageShell>
<Container className="pb-28">
<div className="flex w-full">
<BackButton />
</div>
{shopInfo?._id ? (
<Link
href={`/shops/profile/${shopInfo._id}`}
@@ -282,13 +287,7 @@ export default function ListingDetailClient() {
<div className="flex items-center justify-between gap-2">
<h1 className="text-right text-xl font-bold">{listing.title}</h1>
{discountPercent != null && (
<span className="relative flex h-11 w-11 shrink-0 items-center justify-center">
<span className="absolute inset-0 rounded-lg bg-red-500" />
<span className="absolute inset-0 rotate-[22.5deg] rounded-lg bg-red-500" />
<span className="relative z-10 text-sm font-extrabold text-white">
{t("shops.discountPercentBadge", { percent: discountPercent })}
</span>
</span>
<DiscountBadge percent={discountPercent} className="shrink-0" />
)}
</div>

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
@@ -167,6 +168,9 @@ function ShopContactPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.contactTitle")}</span>

View File

@@ -4,13 +4,21 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import SelectBox from "@/components/elements/SelectBox";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import CountryProvinceCitySelect, {
type GeoSelection,
countryHasProvinces,
findCountry,
findProvince,
findCity,
} from "@/components/ui/CountryProvinceCitySelect";
import useAxios from "@/hooks/useAxios";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import { useShopWizardId } from "@/hooks/useShopWizardId";
import { ICity, IProvince } from "@/types/types";
import { ICity, ICountry, IProvince } from "@/types/types";
import Image from "next/image";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
@@ -24,30 +32,26 @@ function ShopLocationPage() {
const router = useRouter();
const { request, loading } = useAxios();
const shopId = useShopWizardId();
const defaultCountryId = useDefaultCountryId();
const [hasPhysicalLocation, setHasPhysicalLocation] = useState(true);
const [provinces, setProvinces] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [provinceId, setProvinceId] = useState("");
const [cityId, setCityId] = useState("");
const [geoSelection, setGeoSelection] = useState<GeoSelection>({
countryId: null,
provinceId: null,
cityId: null,
});
const [neighbourhood, setNeighbourhood] = useState("");
const [address, setAddress] = useState("");
const [marker, setMarker] = useState<{ lat: number; lng: number } | null>(
null
);
useEffect(() => {
request<{ provinces: IProvince[] }>("GET", "/provinces")
.then((res) => setProvinces(res?.provinces || []))
.catch(() => setProvinces([]));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
if (!shopId) return;
request<{
shop: {
has_physical_location?: boolean;
country?: ICountry | null;
province?: IProvince | null;
city?: ICity | null;
neighbourhood?: string | null;
@@ -59,13 +63,11 @@ function ShopLocationPage() {
const shop = res?.shop;
if (!shop) return;
setHasPhysicalLocation(shop.has_physical_location ?? true);
if (shop.province) {
setProvinceId(String(shop.province.id));
request<{ cities: ICity[] }>("GET", `/cities/${shop.province.id}`)
.then((r) => setCities(r?.cities || []))
.catch(() => setCities([]));
}
if (shop.city) setCityId(String(shop.city.id));
setGeoSelection({
countryId: shop.country?.id ?? defaultCountryId,
provinceId: shop.province?.id ?? null,
cityId: shop.city?.id ?? null,
});
if (shop.neighbourhood) setNeighbourhood(shop.neighbourhood);
if (shop.address) setAddress(shop.address);
if (shop.lat && shop.lng) {
@@ -75,15 +77,6 @@ function ShopLocationPage() {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [shopId]);
const handleProvinceChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const id = e.target.value;
setProvinceId(id);
setCityId("");
request<{ cities: ICity[] }>("GET", `/cities/${id}`)
.then((res) => setCities(res?.cities || []))
.catch(() => setCities([]));
};
const handleMapClick = (event: MapMouseEvent) => {
setMarker({ lat: event.lngLat.lat, lng: event.lngLat.lng });
};
@@ -91,14 +84,16 @@ function ShopLocationPage() {
const handleSubmit = async () => {
if (!shopId) return;
const province = provinces?.find((p) => String(p.id) === provinceId) || null;
const city = cities?.find((c) => String(c.id) === cityId) || null;
const country = findCountry(geoSelection.countryId);
const province = findProvince(geoSelection.provinceId);
const city = findCity(geoSelection.cityId);
const provinceRequired = countryHasProvinces(geoSelection.countryId);
if (!province) {
if (provinceRequired && !province) {
toast.error(t("shops.provinceRequired"));
return;
}
if (!city) {
if (provinceRequired && !city) {
toast.error(t("shops.cityRequired"));
return;
}
@@ -106,6 +101,7 @@ function ShopLocationPage() {
try {
await request("PATCH", `/shops/${shopId}/location`, {
has_physical_location: hasPhysicalLocation,
country,
province,
city,
neighbourhood,
@@ -127,6 +123,9 @@ function ShopLocationPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.locationTitle")}</span>
@@ -141,71 +140,51 @@ function ShopLocationPage() {
<span className="text-sm">{t("shops.noPhysicalShop")}</span>
</label>
<SelectBox value={provinceId} onChange={handleProvinceChange}>
<option disabled value="">
{t("filters.province")}
</option>
{provinces?.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</SelectBox>
<CountryProvinceCitySelect
value={geoSelection}
onChange={setGeoSelection}
/>
<SelectBox
value={cityId}
onChange={(e) => setCityId(e.target.value)}
>
<option disabled value="">
{t("filters.city")}
</option>
{cities?.map((c) => (
<option key={c.id} value={c.id}>
{c.name}
</option>
))}
</SelectBox>
<div className="mt-2 w-full overflow-hidden rounded-2xl">
<Map
style={{ height: "220px" }}
initialViewState={{
longitude: marker?.lng || 51.375433528216654,
latitude: marker?.lat || 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{marker && (
<Marker latitude={marker.lat} longitude={marker.lng}>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src="/images/icons/location.svg"
/>
</Marker>
)}
</Map>
</div>
<div className="mt-2 w-full overflow-hidden rounded-2xl">
<Map
style={{ height: "220px" }}
initialViewState={{
longitude: marker?.lng || 51.375433528216654,
latitude: marker?.lat || 35.73356434056531,
zoom: 11,
}}
mapboxAccessToken="pk.eyJ1IjoiYWxkZjkyIiwiYSI6ImNscHh5ZG56dTByMXAycnJ2cDNmdDQ5M3YifQ.a_sUt1rLFMfA0jHrETcM7A"
mapStyle="mapbox://styles/mapbox/streets-v11"
onClick={handleMapClick}
>
<GeolocateControl />
{marker && (
<Marker latitude={marker.lat} longitude={marker.lng}>
<Image
alt="location icon"
className="-mt-5"
width={25}
height={25}
src="/images/icons/location.svg"
/>
</Marker>
)}
</Map>
</div>
<RoundedInput
type="text"
className="mt-2 text-xs"
placeholder={t("shops.neighbourhoodPlaceholder")}
value={neighbourhood}
onChange={(e) => setNeighbourhood(e.target.value)}
/>
<textarea
className="mt-2 h-24 w-full rounded-3xl border border-neutral-950 bg-white p-4 font-medium dark:border-neutral-400 dark:bg-neutral-950 dark:text-neutral-50"
placeholder={t("shops.addressPlaceholder")}
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
<RoundedInput
type="text"
className="mt-2 text-xs"
placeholder={t("shops.neighbourhoodPlaceholder")}
value={neighbourhood}
onChange={(e) => setNeighbourhood(e.target.value)}
/>
<textarea
className="mt-2 h-24 w-full rounded-3xl border border-neutral-950 bg-white p-4 font-medium dark:border-neutral-400 dark:bg-neutral-950 dark:text-neutral-50"
placeholder={t("shops.addressPlaceholder")}
value={address}
onChange={(e) => setAddress(e.target.value)}
/>
</div>
</AuthPageContent>
<AuthFormFooter>

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
@@ -67,6 +68,9 @@ function ShopLogoPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.logoTitle")}</span>

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
@@ -76,6 +77,9 @@ function ShopNamePage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<form
onSubmit={formik.handleSubmit}
className="flex w-full flex-1 flex-col items-center"

View File

@@ -4,6 +4,7 @@ import AuthPageLayout, {
AuthFormFooter,
AuthPageContent,
} from "@/components/auth/AuthPageLayout";
import BackButton from "@/components/ui/BackButton";
import AuthNextButton from "@/components/auth/AuthNextButton";
import RoundedInput from "@/components/elements/RoundedInput";
import LocalePageShell from "@/components/i18n/LocalePageShell";
@@ -142,6 +143,9 @@ function ShopShippingPage() {
return (
<LocalePageShell>
<AuthPageLayout>
<div className="flex w-full max-w-sm justify-start">
<BackButton />
</div>
<AuthPageContent>
<span className="text-xl font-bold mb-4">{t("shops.shippingTitle")}</span>

View File

@@ -3,6 +3,7 @@
import Container from "@/components/elements/Container";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import useAxios from "@/hooks/useAxios";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useState } from "react";
@@ -52,6 +53,7 @@ export default function ShopPaymentFailedPage() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="mb-4 mt-2 text-center text-lg font-bold text-red-600 md:text-xl">
{t("shops.paymentFailedTitle")}
</h6>

View File

@@ -3,6 +3,7 @@
import Container from "@/components/elements/Container";
import RoundedButton from "@/components/elements/RoundedButton";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { useRouter, useSearchParams } from "next/navigation";
import Image from "next/image";
import { useTranslation } from "react-i18next";
@@ -16,6 +17,7 @@ export default function ShopPaymentSuccessPage() {
return (
<LocalePageShell>
<Container>
<BackButton />
<h6 className="mb-4 mt-2 text-center text-lg font-bold text-green-600 md:text-xl">
{t("shops.paymentSuccessTitle")}
</h6>

View File

@@ -6,7 +6,9 @@ import Header from "@/components/main/Header";
import TabNavigation from "@/components/TabNavigation";
import PageTitle from "@/components/settings/PageTitle";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import DiscountBadge from "@/components/ui/DiscountBadge";
import useAxios from "@/hooks/useAxios";
import { bestDiscountPercent } from "@/lib/shopDiscount";
import { IShopProductListing } from "@/types/types";
import Image from "next/image";
import { useParams, useRouter } from "next/navigation";
@@ -66,6 +68,7 @@ export default function ProductComparisonPage() {
listing.images?.[listing.primaryImageIndex] ||
listing.images?.[0];
const price = cheapestPrice(listing);
const discountPercent = bestDiscountPercent(listing);
return (
<button
@@ -84,6 +87,12 @@ export default function ProductComparisonPage() {
sizes="64px"
unoptimized
/>
{discountPercent != null && (
<DiscountBadge
percent={discountPercent}
className="absolute right-1 top-1"
/>
)}
</div>
)}
<div className="min-w-0 flex-1 text-right">

View File

@@ -4,6 +4,7 @@ import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import Header from "@/components/main/Header";
import TabNavigation from "@/components/TabNavigation";
import BackButton from "@/components/ui/BackButton";
import { IMAGE_BASE_URL } from "@/components/main/BaseUrl";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import ExpandableBio from "@/components/profile/ExpandableBio";
@@ -170,6 +171,9 @@ export default function ShopProfileClient() {
<Header />
<LocalePageShell>
<Container className="pb-28">
<div className="mx-auto flex w-full max-w-md">
<BackButton />
</div>
<div className="mx-auto w-full max-w-md" dir={isFa ? "ltr" : "rtl"}>
<div className="flex w-full items-center justify-between py-2">
<div className="flex flex-col items-center text-xs md:text-sm">

View File

@@ -19,7 +19,7 @@ function CommentsModal({
onCommentAdded,
}: CommentsModalProps) {
return (
<Modal isOpen={isOpen} onClose={onClose} height="min(82vh, 520px)">
<Modal isOpen={isOpen} onClose={onClose} height="min(82vh, 520px)" elevated>
<CourseCommentsContent
courseId={postData._id}
courseName={postData.cuorse_name}

View File

@@ -13,6 +13,7 @@ import Rate from "rc-rate";
import "rc-rate/assets/index.css";
import "@/styles/rc-rate-custom.css";
import BoldIcon from "@/components/ui/BoldIcon";
import CommentsSkeleton from "@/components/ui/CommentsSkeleton";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import { useRequireAuth } from "@/lib/auth/useRequireAuth";
import { useUser } from "@/hooks/useUser";
@@ -306,7 +307,9 @@ export default function CourseCommentsContent({
</div>
<div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
{comments.length === 0 && !loading ? (
{comments.length === 0 && loading ? (
<CommentsSkeleton />
) : comments.length === 0 && !loading ? (
<p className="py-8 text-center text-gray-500">{t("posts.noComments")}</p>
) : (
comments.map((item, index) => (
@@ -323,7 +326,7 @@ export default function CourseCommentsContent({
</div>
))
)}
{loading ? (
{loading && comments.length > 0 ? (
<p className="py-4 text-center text-sm text-neutral-400">
{t("common.loading")}
</p>

View File

@@ -17,10 +17,10 @@ import {
reorderCaptionLinksToEnd,
splitCaptionBodyAndLinks,
} from "@/lib/postLinkCaption";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
import { buildStorageUrl } from "@/components/main/BaseUrl";
import VerificationBadge from "@/components/main/VerificationBadge";
import ScoreRateStats from "@/components/main/ScoreRateStats";
import BoldIcon from "@/components/ui/BoldIcon";
import DiscountBadge from "@/components/ui/DiscountBadge";
import { useUserById } from "@/hooks/getUserById";
import "slick-carousel/slick/slick.css";
import "slick-carousel/slick/slick-theme.css";
@@ -404,7 +404,7 @@ function MainModelCard({
<Image
src={
academy?.academy_image
? IMAGE_BASE_URL + academy?.academy_image
? buildStorageUrl(academy.academy_image)
: "/images/fake-avatar.png"
}
alt={cuorse_name}
@@ -429,15 +429,7 @@ function MainModelCard({
<span className="text-[#0090ff] text-xl font-semibold flex items-center">
{formatCoursePriceLabel(postData, priceLang)}
{!postData.is_free && postData.offer && Number(postData.offer) > 0 ? (
<span className="mr-2 inline-flex items-center gap-1 rounded-full bg-[#ff5c00] px-2 py-0.5 text-xs font-bold text-white">
{t("shops.discountPercent", { percent: postData.offer })}
<BoldIcon
name="receipt-discount"
size={12}
tinted
className="text-white"
/>
</span>
<DiscountBadge percent={Number(postData.offer)} className="mr-2" />
) : null}
</span>
</div>
@@ -466,7 +458,7 @@ function MainModelCard({
<div className="w-full flex-shrink-0 flex justify-center items-center">
<div className="w-full rounded-2xl overflow-hidden shadow-[0_8px_30px_rgba(0,0,0,0.12)] dark:shadow-[0_8px_30px_rgba(0,0,0,0.45)]">
<Image
src={IMAGE_BASE_URL + course_image}
src={buildStorageUrl(course_image)}
alt={`media-${course_image}`}
width={1000}
height={1000}

View File

@@ -1,11 +1,16 @@
"use client";
import { useState } from "react";
import { useAppLanguage } from "@/contexts/LanguageProvider";
import { ENABLED_LANGUAGES, LANGUAGE_LABELS, type AppLanguage } from "@/lib/i18n/registry";
import { PRIMARY_LANGUAGES, LANGUAGE_LABELS, type AppLanguage } from "@/lib/i18n/registry";
import { cn } from "@/lib/utils";
import { useTranslation } from "react-i18next";
import OtherLanguagesModal from "./OtherLanguagesModal";
export default function AuthLanguageToggle({ className }: { className?: string }) {
const { t } = useTranslation("common");
const { language, setLanguage } = useAppLanguage();
const [modalOpen, setModalOpen] = useState(false);
const handleChange = (next: AppLanguage) => {
if (next === language) return;
@@ -13,25 +18,35 @@ export default function AuthLanguageToggle({ className }: { className?: string }
};
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>
<>
<div className={cn("flex items-center gap-1.5", className)}>
{PRIMARY_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>
);
})}
<button
type="button"
onClick={() => setModalOpen(true)}
className="rounded-full border border-neutral-200 px-3 py-1 text-xs font-semibold text-neutral-500 transition-colors dark:border-neutral-700 dark:text-neutral-400"
>
{t("language.otherLanguages")}
</button>
</div>
<OtherLanguagesModal open={modalOpen} onClose={() => setModalOpen(false)} />
</>
);
}

View File

@@ -71,7 +71,7 @@ function AuthPageLayout({ children, className }: AuthPageLayoutProps) {
<Container>
<div
className={cn(
"flex min-h-screen flex-col items-center py-4",
"flex min-h-dvh flex-col items-center py-4",
className
)}
>

View File

@@ -0,0 +1,110 @@
"use client";
import Modal from "@/components/elements/Modal";
import RoundedInput from "@/components/elements/RoundedInput";
import { useAppLanguage } from "@/contexts/LanguageProvider";
import {
SUPPORTED_LANGUAGES,
LANGUAGE_REGISTRY,
type AppLanguage,
} from "@/lib/i18n/registry";
import { useMemo, useState } from "react";
import toast from "react-hot-toast";
import { useTranslation } from "react-i18next";
type OtherLanguagesModalProps = {
open: boolean;
onClose: () => void;
};
export default function OtherLanguagesModal({
open,
onClose,
}: OtherLanguagesModalProps) {
const { t } = useTranslation("common");
const { language, setLanguage } = useAppLanguage();
const [query, setQuery] = useState("");
const languages = useMemo(() => {
const q = query.trim().toLowerCase();
return SUPPORTED_LANGUAGES.map((code) => LANGUAGE_REGISTRY[code]).filter(
(def) =>
!q ||
def.nativeLabel.toLowerCase().includes(q) ||
def.englishLabel.toLowerCase().includes(q)
);
}, [query]);
const handleSelect = async (code: AppLanguage) => {
const def = LANGUAGE_REGISTRY[code];
if (!def.enabled) {
toast(t("language.comingSoon"));
return;
}
if (code !== language) {
await setLanguage(code);
}
onClose();
};
return (
<Modal
isOpen={open}
onClose={onClose}
height="fit"
elevated
panelClassName="!p-0 max-h-[80dvh] w-full overflow-y-auto overscroll-y-contain touch-pan-y [-webkit-overflow-scrolling:touch]"
>
<div className="px-4 pt-5 pb-[calc(2rem+env(safe-area-inset-bottom,0px))]">
<p className="mb-4 text-center font-semibold">
{t("language.selectTitle")}
</p>
<RoundedInput
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder={t("language.searchPlaceholder")}
className="mb-4"
/>
{languages.length === 0 ? (
<p className="py-10 text-center text-sm text-neutral-500">
{t("language.noResults")}
</p>
) : (
<div className="flex flex-col gap-2">
{languages.map((def) => {
const active = def.code === language;
return (
<button
key={def.code}
type="button"
dir={def.direction}
onClick={() => void handleSelect(def.code as AppLanguage)}
className={`flex items-center justify-between rounded-xl border px-4 py-3 text-right transition-colors ${
active
? "border-[#0095f6] bg-[#0095f6]/10"
: "border-neutral-200 dark:border-neutral-800"
} ${!def.enabled ? "opacity-60" : ""}`}
>
<div className="flex flex-col items-start">
<span className="text-sm font-semibold">
{def.nativeLabel}
</span>
<span className="text-xs text-neutral-500">
{def.englishLabel}
</span>
</div>
{!def.enabled && (
<span className="rounded-full bg-neutral-100 px-2 py-0.5 text-[10px] font-semibold text-neutral-500 dark:bg-neutral-800">
{t("language.comingSoonBadge")}
</span>
)}
</button>
);
})}
</div>
)}
</div>
</Modal>
);
}

View File

@@ -4,6 +4,7 @@ import AdProfileContent from "@/components/billboards/Profile/AdProfileContent";
import AdProfileHead from "@/components/billboards/Profile/AdProfileHead";
import Container from "@/components/elements/Container";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import BackButton from "@/components/ui/BackButton";
import { IAdvertisingProfile } from "@/types/types";
import { useTranslation } from "react-i18next";
@@ -26,6 +27,7 @@ export default function AdvertisingProfileContent({
{profile.city?.name}, {profile.neighbourhood}
</h1>
<BackButton />
<AdProfileHead profile={profile} id={id} />
<AdProfileContent profile={profile} />
</Container>

View File

@@ -5,6 +5,7 @@ import BillboardImageSlider from "@/components/billboards/BillboardPage/Billboar
import BillboardDetails from "@/components/billboards/BillboardPage/BillboardDetails";
import LocalePageShell from "@/components/i18n/LocalePageShell";
import MainBillboardCardActions from "@/components/billboards/MainBillboardCard/MainBillboardCardActions";
import BackButton from "@/components/ui/BackButton";
import { IAdvertising, IRate } from "@/types/types";
import { trackExploreInteraction } from "@/api/trackExploreInteraction";
import Cookies from "js-cookie";
@@ -43,6 +44,7 @@ export default function BillboardDetailContent({
return (
<LocalePageShell>
<Container>
<BackButton />
<a
href={`/billboards/profile/${billboard?.creatorId}/${encodeURIComponent(billboard?.title || "")}`}
>

View File

@@ -8,6 +8,7 @@ import SelectBox from "@/components/elements/SelectBox";
import RoundedButton from "@/components/elements/RoundedButton";
import { btnHeightMd, btnHeightSm, toggleBtnClass } from "@/lib/ui/buttonStyles";
import { cn } from "@/lib/utils";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import { useTranslation } from "react-i18next";
interface IFilterProps {
@@ -46,6 +47,7 @@ function FilterModal({
}: IFilterProps) {
const { t } = useTranslation("common");
const { request } = useAxios();
const defaultCountryId = useDefaultCountryId();
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [categories, setCategories] = useState<ICategory[] | null>(null);
@@ -54,12 +56,12 @@ function FilterModal({
const fetchStates = async () => {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
defaultCountryId ? `/provinces?country_id=${defaultCountryId}` : "/provinces"
);
setAllStates(response?.provinces || null);
};
fetchStates();
}, [request]);
}, [request, defaultCountryId]);
useEffect(() => {
const fetchCategories = async () => {

View File

@@ -2,11 +2,12 @@
import React, { useState } from "react";
import Modal from "../../elements/Modal";
import Image from "next/image";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import VerificationBadge from "@/components/main/VerificationBadge";
import ProfileAvatar from "@/components/main/ProfileAvatar";
import RoundedInput from "@/components/elements/RoundedInput";
import BoldIcon from "@/components/ui/BoldIcon";
import CommentsSkeleton from "@/components/ui/CommentsSkeleton";
import useAxios from "@/hooks/useAxios";
import { useRequireAuth } from "@/lib/auth/useRequireAuth";
import { useTranslation } from "react-i18next";
@@ -45,7 +46,7 @@ function BillboardCommentsModal({
advertisingId,
}: ReportModalProps) {
const { t } = useTranslation("common");
const { data, containerRef, isFetchingNextPage } = useInfiniteScroll({
const { data, containerRef, isFetchingNextPage, isLoading, refetch } = useInfiniteScroll({
endpoint: "/advertising/comments",
queryKey: ["comments", advertisingId],
params: { advertisingId: advertisingId },
@@ -54,87 +55,91 @@ function BillboardCommentsModal({
const { request } = useAxios();
const requireAuth = useRequireAuth();
const comments: Comments[] =
data?.pages?.flatMap((page) => page?.comments || []) || [];
const sendMessageHandler = async () => {
if (!requireAuth()) return;
if (!requireAuth() || !newMessage.trim()) return;
try {
const data = {
await request("POST", "/advertising/comment", {
text: newMessage,
advertisingId: advertisingId,
};
await request("POST", "/advertising/comment", data);
});
setNewMessage("");
refetch?.();
} catch (error) {
console.log(error);
}
};
return (
<div
onClick={(e) => {
e.stopPropagation();
}}
>
<Modal isOpen={isOpen} onClose={onClose} height="540px">
<div
ref={containerRef}
className="overflow-y-auto max-h-[500px] p-4 pb-16"
>
{data?.pages.length === 0 ||
(data?.pages[0]?.comments?.length === 0 && !isFetchingNextPage) ? (
<p className="text-center text-gray-500">{t("billboards.comments.empty")}</p>
<Modal isOpen={isOpen} onClose={onClose} height="min(82vh, 520px)" elevated>
<div className="flex h-full flex-col">
<div className="flex shrink-0 items-center justify-center border-b border-neutral-200/60 px-4 py-3 dark:border-neutral-700">
<h3 className="font-bold">{t("posts.comments")}</h3>
</div>
<div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto px-4 py-3">
{isLoading ? (
<CommentsSkeleton />
) : comments.length === 0 && !isFetchingNextPage ? (
<p className="py-8 text-center text-gray-500">{t("billboards.comments.empty")}</p>
) : (
data?.pages?.map((page, pageIndex) => (
<React.Fragment key={pageIndex}>
{page?.comments?.map((item: Comments) => (
<div key={item?._id} className="mb-4">
<div className="flex">
<ProfileAvatar
src={item?.userId?.profile_image}
alt={item?.userId?.user_name || t("common.user")}
size="chat"
rounded="full"
className="ml-2"
userId={item?.userId?._id}
verifyBadge={item?.userId?.verify_badge}
/>
<div className="text-sm">
<div className="flex items-center gap-1 mt-1 font-semibold">
<span className="inline-flex items-center gap-1">
{item?.userId?.user_name}
<VerificationBadge verifyBadge={item?.userId?.verify_badge} />
</span>
</div>
<p>{item.text}</p>
</div>
</div>
comments.map((item) => (
<div
key={item._id}
className="mb-3 flex gap-2 border-b border-neutral-100 pb-2 dark:border-neutral-800"
>
<ProfileAvatar
src={item?.userId?.profile_image}
alt={item?.userId?.user_name || t("common.user")}
size="chat"
rounded="full"
className="shrink-0"
userId={item?.userId?._id}
verifyBadge={item?.userId?.verify_badge}
/>
<div className="min-w-0 flex-1 text-sm">
<div className="flex items-center gap-2">
<span className="inline-flex items-center gap-1 font-semibold">
{item?.userId?.user_name}
<VerificationBadge verifyBadge={item?.userId?.verify_badge} />
</span>
</div>
))}
</React.Fragment>
<p className="mt-0.5 break-words text-neutral-800 dark:text-neutral-100">
{item.text}
</p>
</div>
</div>
))
)}
{isFetchingNextPage && (
<p className="text-center text-gray-500">
<p className="py-4 text-center text-sm text-neutral-400">
{t("billboards.comments.loading")}
</p>
)}
<div className="flex items-center w-full p-4 rounded-full max-w-md fixed bottom-24 left-1/2 -translate-x-1/2 px-4 gap-2 ">
<button onClick={sendMessageHandler}>
<Image
width={24}
height={24}
alt={"verify icon"}
src={`/images/icons/send-msg.svg`}
className="dark:invert min-w-[24px] min-h-[24px]"
/>
</div>
<div className="shrink-0 border-t border-neutral-200/60 px-4 pt-3 pb-[calc(0.75rem+env(safe-area-inset-bottom))] dark:border-neutral-700">
<div className="flex items-center gap-2">
<button
type="button"
onClick={() => void sendMessageHandler()}
className="ig-dm-send-btn gentle-transition flex h-10 w-10 shrink-0 items-center justify-center rounded-full text-white active:scale-90"
>
<BoldIcon name="send-2" size={18} tinted className="text-current" />
</button>
<RoundedInput
value={newMessage}
placeholder={t("posts.writeComment")}
onChange={(e) => setNewMessage(e.target.value)}
className="flex-1"
/>
</div>
</div>
</Modal>
</div>
</div>
</Modal>
);
}

View File

@@ -1,6 +1,7 @@
"use client";
import { useState } from "react";
import BackButton from "@/components/ui/BackButton";
import Step1 from "./Step1";
import Step2 from "./Step2";
import Step3 from "./Step3";
@@ -12,6 +13,9 @@ const MultiStepForm = () => {
return (
<div className="p-4 flex flex-col items-center w-full">
<div className="flex w-full max-w-sm justify-start">
<BackButton onClick={() => (step > 1 ? setStep(step - 1) : window.history.back())} />
</div>
{step === 1 && <Step1 nextStep={() => setStep(2)} />}
{step === 2 && <Step2 nextStep={() => setStep(3)} />}
{step === 3 && <Step3 nextStep={() => setStep(4)} />}

View File

@@ -5,12 +5,14 @@ import { useFormik } from "formik";
import * as Yup from "yup";
import LocationSelector from "./step1/LocationSelector";
import { useBillboardForm } from "@/contexts/BillboardFormContext";
import { useEffect, useMemo, useState } from "react";
import { useEffect, useMemo, useRef, useState } from "react";
import useAxios from "@/hooks/useAxios";
import SelectBox from "@/components/elements/SelectBox";
import { IAdvertisingCategory } from "@/types/types";
import RoundedInput from "@/components/elements/RoundedInput";
import ImageUploader from "./step1/ImageUploader";
import { countryHasProvinces } from "@/components/ui/CountryProvinceCitySelect";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import { useTranslation } from "react-i18next";
const Step1 = ({ nextStep }: { nextStep: () => void }) => {
@@ -18,12 +20,26 @@ const Step1 = ({ nextStep }: { nextStep: () => void }) => {
const { formData, updateForm } = useBillboardForm();
const [categories, setCategories] = useState<IAdvertisingCategory[]>([]);
const { request } = useAxios();
const defaultCountryId = useDefaultCountryId();
const seededDefault = useRef(false);
const validationSchema = useMemo(
() =>
Yup.object({
stateId: Yup.string().required(t("billboards.form.validation.provinceRequired")),
cityId: Yup.string().required(t("billboards.form.validation.cityRequired")),
stateId: Yup.string().when("countryId", {
is: (countryId: string) =>
countryHasProvinces(countryId ? Number(countryId) : null),
then: (schema) =>
schema.required(t("billboards.form.validation.provinceRequired")),
otherwise: (schema) => schema,
}),
cityId: Yup.string().when("countryId", {
is: (countryId: string) =>
countryHasProvinces(countryId ? Number(countryId) : null),
then: (schema) =>
schema.required(t("billboards.form.validation.cityRequired")),
otherwise: (schema) => schema,
}),
categoryId: Yup.string().required(t("billboards.form.validation.categoryRequired")),
adsTitle: Yup.string().required(t("billboards.form.validation.titleRequired")),
description: Yup.string().required(t("billboards.form.validation.descriptionRequired")),
@@ -58,6 +74,7 @@ const Step1 = ({ nextStep }: { nextStep: () => void }) => {
const formik = useFormik({
initialValues: {
countryId: formData.countryId || "",
cityId: formData.cityId || "",
stateId: formData.stateId || "",
adsTitle: formData.adsTitle || "",
@@ -75,6 +92,13 @@ const Step1 = ({ nextStep }: { nextStep: () => void }) => {
},
});
useEffect(() => {
if (seededDefault.current || formik.values.countryId || !defaultCountryId) return;
seededDefault.current = true;
formik.setFieldValue("countryId", String(defaultCountryId));
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [defaultCountryId]);
return (
<form
onSubmit={formik.handleSubmit}

View File

@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
"use client";
import { ICity, IProvince } from "@/types/types";
import SelectBox from "@/components/elements/SelectBox";
import CountryProvinceCitySelect, {
type GeoSelection,
} from "@/components/ui/CountryProvinceCitySelect";
import { useState, useEffect } from "react";
import useAxios from "@/hooks/useAxios";
import RoundedInput from "@/components/elements/RoundedInput";
import Map, { GeolocateControl, Marker } from "react-map-gl";
import "mapbox-gl/dist/mapbox-gl.css";
@@ -20,61 +20,33 @@ const LocationSelector: React.FC<LocationSelectorProps> = ({ formik }) => {
const { t } = useTranslation("common");
const { formData } = useBillboardForm();
const { request } = useAxios();
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [selectedLocation, setSelectedLocation] = useState<{
lat: number;
lng: number;
} | null>(null);
const fetchStates = async () => {
try {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
);
setAllStates(response?.provinces || null);
} catch (err) {
console.log(err);
}
const geoSelection: GeoSelection = {
countryId: formik.values.countryId ? Number(formik.values.countryId) : null,
provinceId: formik.values.stateId ? Number(formik.values.stateId) : null,
cityId: formik.values.cityId ? Number(formik.values.cityId) : null,
};
const fetchCities = async (provinceId: string) => {
try {
const response = await request<{ cities: ICity[] }>(
"GET",
`/cities/${provinceId}`
);
setCities(response?.cities || []);
} catch (err) {
console.log(err);
}
const handleGeoChange = (next: GeoSelection) => {
formik.setFieldValue("countryId", next.countryId ? String(next.countryId) : "");
formik.setFieldValue("stateId", next.provinceId ? String(next.provinceId) : "");
formik.setFieldValue("cityId", next.cityId ? String(next.cityId) : "");
};
useEffect(() => {
fetchStates();
}, []);
useEffect(() => {
if (formik.values.stateId) {
fetchCities(formik.values.stateId);
}
if (formData?.markerCoordinate && formData?.markerCoordinate[1]) {
setSelectedLocation({
lat: Number(formData?.markerCoordinate[1]),
lng: Number(formData?.markerCoordinate[0]),
});
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [formik?.values?.markerCoordinate]);
const handleProvinceChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const selectedProvinceId = e.target.value;
formik.setFieldValue("stateId", selectedProvinceId);
fetchCities(selectedProvinceId);
};
const handleMapClick = (event: any) => {
const { lngLat } = event;
setSelectedLocation({
@@ -86,48 +58,16 @@ const LocationSelector: React.FC<LocationSelectorProps> = ({ formik }) => {
return (
<>
<SelectBox
className={`max-w-full ${
formik.touched.stateId && formik.errors.stateId
? "border-red-500 dark:border-red-500"
: ""
}`}
value={formik.values.stateId}
onChange={handleProvinceChange}
>
<option disabled value="">
{t("billboards.province")}
</option>
{allStates?.map((item: IProvince) => (
<option key={item.id} value={item.id}>
{item?.name}
</option>
))}
</SelectBox>
<CountryProvinceCitySelect
value={geoSelection}
onChange={handleGeoChange}
className="w-full max-w-full"
/>
{formik.touched.stateId && formik.errors.stateId && (
<small className="text-red-500 block text-center">
{formik.errors.stateId}
</small>
)}
<SelectBox
className={`max-w-full ${
formik.touched.cityId && formik.errors.cityId
? "border-red-500 dark:border-red-500"
: "border-gray-300"
}`}
value={formik.values.cityId}
onChange={(e) => formik.setFieldValue("cityId", e.target.value)}
>
<option disabled value="">
{t("billboards.city")}
</option>
{cities?.map((city: ICity) => (
<option key={city.id} value={city.id}>
{city.name}
</option>
))}
</SelectBox>
{formik.touched.cityId && formik.errors.cityId && (
<small className="text-red-500 block text-center">
{formik.errors.cityId}

View File

@@ -25,6 +25,7 @@ export async function submitBillboard(
adsTitle,
categoryId,
description,
countryId,
stateId,
cityId,
neighbourhood,
@@ -47,6 +48,7 @@ export async function submitBillboard(
if (description) {
data.append("description", description);
}
data.append("country", countryId ? countryId : "");
data.append("province", stateId ? stateId : "");
data.append("city", cityId ? cityId : "");
if (neighbourhood) {

View File

@@ -185,6 +185,7 @@ function PreviewOverlay({
src={preview}
controls
autoPlay
muted
playsInline
className="max-h-[88vh] rounded-2xl shadow-2xl"
/>
@@ -263,6 +264,34 @@ const ChatMessageCard = ({
} | null>(null);
const [viewOnceVoiceUrl, setViewOnceVoiceUrl] = useState<string | null>(null);
const { openViewOnce, completeViewOnce } = useViewOnceMedia();
const finishViewOnce = useCallback(async () => {
setViewOncePreview(null);
setViewOnceVoiceUrl(null);
if (message._id.startsWith("temp")) return;
try {
await completeViewOnce(message._id);
} catch {
toast.error(t("chats.toast.viewOnceDeleteFailed"));
}
}, [completeViewOnce, message._id, t]);
const handleViewOnceOpen = useCallback(async () => {
if (message._id.startsWith("temp")) return;
try {
const { url, fileType: openedType } = await openViewOnce(message._id);
if (openedType === "voice") {
setViewOnceVoiceUrl(url);
} else if (openedType === "video") {
setViewOncePreview({ url, type: "video" });
} else {
setViewOncePreview({ url, type: "image" });
}
} catch {
toast.error(t("chats.toast.viewOnceOpenFailed"));
}
}, [message._id, openViewOnce, t]);
const canSelectForDelete =
deleteMode &&
isSender &&
@@ -291,6 +320,7 @@ const ChatMessageCard = ({
swipeEnabled && "onDragStart" in dragProps ? dragProps.onDragStart : undefined;
const restDragProps = swipeEnabled
? (() => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { onDragStart: _, ...rest } = dragProps;
return rest;
})()
@@ -380,33 +410,6 @@ const ChatMessageCard = ({
(message.viewOnceLocked || message.viewOnceExpired) &&
!viewOnceVoiceUrl;
const finishViewOnce = useCallback(async () => {
setViewOncePreview(null);
setViewOnceVoiceUrl(null);
if (message._id.startsWith("temp")) return;
try {
await completeViewOnce(message._id);
} catch {
toast.error(t("chats.toast.viewOnceDeleteFailed"));
}
}, [completeViewOnce, message._id]);
const handleViewOnceOpen = useCallback(async () => {
if (message._id.startsWith("temp")) return;
try {
const { url, fileType: openedType } = await openViewOnce(message._id);
if (openedType === "voice") {
setViewOnceVoiceUrl(url);
} else if (openedType === "video") {
setViewOncePreview({ url, type: "video" });
} else {
setViewOncePreview({ url, type: "image" });
}
} catch {
toast.error(t("chats.toast.viewOnceOpenFailed"));
}
}, [message._id, openViewOnce]);
const TimeInline = () => (
<span className="message-time-inline">
{timeLabel}
@@ -839,6 +842,8 @@ const ChatMessageCard = ({
src={viewOncePreview.url}
controls
autoPlay
muted
playsInline
className="max-h-[88vh] rounded-lg"
/>
) : (

View File

@@ -7,6 +7,7 @@ import RoundedButton from "@/components/elements/RoundedButton";
import RoundedInput from "@/components/elements/RoundedInput";
import SelectBox from "@/components/elements/SelectBox";
import useAxios from "@/hooks/useAxios";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
import { ICity, IExpertise, IProvince } from "@/types/types";
import toast from "react-hot-toast";
import BoldIcon from "@/components/ui/BoldIcon";
@@ -30,6 +31,7 @@ type RoomVisibility = "public" | "private";
export default function CreateChatRoomModal({ open, onClose, onCreated }: Props) {
const { t } = useTranslation("common");
const { request } = useAxios();
const defaultCountryId = useDefaultCountryId();
const fileRef = useRef<HTMLInputElement>(null);
const [title, setTitle] = useState("");
const [visibility, setVisibility] = useState<RoomVisibility>("public");
@@ -57,14 +59,17 @@ export default function CreateChatRoomModal({ open, onClose, onCreated }: Props)
setImageFile(null);
setImagePreview(null);
void request<{ provinces: IProvince[] }>("GET", "/provinces", null, {
noToast: true,
}).then((res) => setProvinces(res?.provinces ?? []));
void request<{ provinces: IProvince[] }>(
"GET",
defaultCountryId ? `/provinces?country_id=${defaultCountryId}` : "/provinces",
null,
{ noToast: true }
).then((res) => setProvinces(res?.provinces ?? []));
void request<{ expertises: IExpertise[] }>("GET", "/expertise", null, {
noToast: true,
}).then((res) => setExpertiseList(res?.expertises ?? []));
}, [open, request]);
}, [open, request, defaultCountryId]);
useEffect(() => {
if (!provinceId) {

View File

@@ -3,10 +3,10 @@
import { Course } from "@/types/types";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Edit, Trash2, Video, Clock, DollarSign, Highlighter, ChevronLeft, ChevronRight } from "lucide-react";
import Image from "next/image";
import { IMAGE_BASE_URL } from "../main/BaseUrl";
import DiscountBadge from "@/components/ui/DiscountBadge";
import { buildStorageUrl } from "../main/BaseUrl";
import PageLoader from "@/components/ui/PageLoader";
import { useTranslation } from "react-i18next";
@@ -60,7 +60,7 @@ export function CourseList({
<Card key={course._id || index} className={`overflow-hidden hover:shadow-lg transition-shadow ${course.type === "pro" ? "shadow-lg shadow-[#FF107D] border-2 border-[#FF107D]" : ""}`}>
<div className="relative h-48 w-full">
<Image
src={IMAGE_BASE_URL + course.course_image}
src={buildStorageUrl(course.course_image)}
alt={course.cuorse_name}
fill
className="object-cover"
@@ -69,9 +69,10 @@ export function CourseList({
}}
/>
{course.offer && parseInt(course.offer) > 0 && (
<Badge className="absolute top-2 right-2 bg-red-500">
{t("academyCourse.discountBadge", { percent: course.offer })}
</Badge>
<DiscountBadge
percent={parseInt(course.offer)}
className="absolute right-2 top-2"
/>
)}
</div>

View File

@@ -17,6 +17,7 @@ import { Course } from "@/types/types";
import { toast } from "react-hot-toast";
import useAxios from "@/hooks/useAxios";
import SubscriptionCard from "./SubscriptionCard";
import BackButton from "@/components/ui/BackButton";
import Cookies from "js-cookie";
import { useTranslation } from "react-i18next";
function appendCourseFormFields(formData: FormData, data: any) {
@@ -454,6 +455,7 @@ export function CourseManager() {
return (
<div className="container mx-auto px-4 py-6 lg:px-6">
<BackButton className="mb-4" />
<div className="flex justify-between items-center mb-6">
<h1 className="text-3xl font-bold">{t("academyCourse.manageTitle")}</h1>
<Button

View File

@@ -22,6 +22,7 @@ import { Post } from "@/types/types";
import { MutedProvider } from "@/components/models/ModelPage/MainModelCardPost";
import ReelsPostCard from "@/components/posts/ReelsPostCard";
import ReelsAcademyCard from "@/components/explore/ReelsAcademyCard";
import BackButton from "@/components/ui/BackButton";
import PageLoader from "@/components/ui/PageLoader";
import { ExploreFilterId } from "@/constants/exploreFilters";
import { buildExplorePostFilters } from "@/lib/explore/buildPostFilters";
@@ -385,6 +386,11 @@ export default function ExploreReelsView({
<ExploreReelsUiProvider>
<MutedProvider>
<div className="relative min-h-[100dvh] bg-black">
<BackButton
tinted
iconSize={22}
className="fixed right-4 top-[calc(1rem+env(safe-area-inset-top))] z-[100] bg-black/50 text-white"
/>
<div className={REELS_SHELL_CLASS}>
<div
ref={scrollRef}

View File

@@ -4,6 +4,7 @@ import { Service } from "@/types/types";
import Image from "next/image";
import React from "react";
import { IMAGE_BASE_URL } from "../BaseUrl";
import DiscountBadge from "@/components/ui/DiscountBadge";
import { useTranslation } from "react-i18next";
interface ServiceItemProps {
@@ -66,8 +67,8 @@ const ServiceItem: React.FC<ServiceItemProps> = ({ service, onDelete }) => {
<div className="font-semibold text-xs md:text-sm mr-4 min-w-16">
{Number(discountPrice).toLocaleString()} {t("settings.toman")}
</div>
<div className="text-xs md:text-sm bg-red-600 text-white w-10 h-7 flex items-center justify-center rounded-full pt-1 mr-2">
{discountPercentage}%
<div className="mr-2">
<DiscountBadge percent={discountPercentage} />
</div>
</>
) : (

View File

@@ -15,6 +15,7 @@ import AppearanceColorSelect from "./AppearanceColorSelect";
import { isModelExpertise } from "@/lib/isModelExpertise";
import RoundedButton from "../elements/RoundedButton";
import { filterChipClass } from "@/lib/ui/buttonStyles";
import { useDefaultCountryId } from "@/hooks/useDefaultCountryId";
interface IFilterProps {
setShowFilterModal: (e: boolean) => void;
@@ -133,6 +134,7 @@ function FilterModal({
}: IFilterProps) {
const { t } = useTranslation("common");
const { request } = useAxios();
const defaultCountryId = useDefaultCountryId();
const [allStates, setAllStates] = useState<IProvince[] | null>(null);
const [cities, setCities] = useState<ICity[] | null>(null);
const [expertiseList, setExpertiseList] = useState<IExpertise[]>([]);
@@ -142,12 +144,12 @@ function FilterModal({
const fetchStates = async () => {
const response = await request<{ provinces: IProvince[] }>(
"GET",
"/provinces"
defaultCountryId ? `/provinces?country_id=${defaultCountryId}` : "/provinces"
);
setAllStates(response?.provinces || null);
};
fetchStates();
}, []);
}, [defaultCountryId]);
useEffect(() => {
if (!showFilterModal) return;

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