reels
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function CommentsModal({
|
||||
postId,
|
||||
}: CommentsModalProps) {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} height="min(82vh, 520px)">
|
||||
<Modal isOpen={isOpen} onClose={onClose} height="min(82vh, 520px)" elevated>
|
||||
<PostCommentsContent
|
||||
userId={userId}
|
||||
postId={postId}
|
||||
|
||||
@@ -434,7 +434,7 @@ function MainModelCard({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="pointer-events-none absolute left-3 top-3 z-50 rounded-full bg-black/50 p-2 backdrop-blur-md">
|
||||
<div className="pointer-events-none absolute bottom-3 left-3 z-50 rounded-full bg-black/50 p-2 backdrop-blur-md">
|
||||
{videoMuted ? (
|
||||
<BoldIcon name="volume-slash" size={20} tinted className="text-white" aria-label={t("models.mute")} />
|
||||
) : (
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user