reels
This commit is contained in:
3
public/images/icons/back-arrow.svg
Normal file
3
public/images/icons/back-arrow.svg
Normal 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 |
12
public/images/icons/discount-badge.svg
Normal file
12
public/images/icons/discount-badge.svg
Normal 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 |
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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")}{" "}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 ? (
|
||||
<>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 */}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}`}
|
||||
>
|
||||
|
||||
@@ -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 مکرر
|
||||
|
||||
@@ -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" />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
129
src/app/settings/profile/language/page.tsx
Normal file
129
src/app/settings/profile/language/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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 />
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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)} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
>
|
||||
|
||||
110
src/components/auth/OtherLanguagesModal.tsx
Normal file
110
src/components/auth/OtherLanguagesModal.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 || "")}`}
|
||||
>
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)} />}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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"
|
||||
/>
|
||||
) : (
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
) : (
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user