diff --git a/src/components/NewBillboard/MultiStepForm.tsx b/_dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx similarity index 96% rename from src/components/NewBillboard/MultiStepForm.tsx rename to _dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx index 7907deb..cad7483 100644 --- a/src/components/NewBillboard/MultiStepForm.tsx +++ b/_dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx @@ -1,24 +1,24 @@ -"use client"; - -import { useState } from "react"; -import Step1 from "./Step1"; -import Step2 from "./Step2"; -import Step3 from "./Step3"; -import Step4 from "./Step4"; -import Step5 from "./Step5"; - -const MultiStepForm = () => { - const [step, setStep] = useState(1); - - return ( -
- {step === 1 && setStep(2)} />} - {step === 2 && setStep(3)} />} - {step === 3 && setStep(4)} />} - {step === 4 && setStep(5)} />} - {step === 5 && } -
- ); -}; - -export default MultiStepForm; +"use client"; + +import { useState } from "react"; +import Step1 from "./Step1"; +import Step2 from "./Step2"; +import Step3 from "./Step3"; +import Step4 from "./Step4"; +import Step5 from "./Step5"; + +const MultiStepForm = () => { + const [step, setStep] = useState(1); + + return ( +
+ {step === 1 && setStep(2)} />} + {step === 2 && setStep(3)} />} + {step === 3 && setStep(4)} />} + {step === 4 && setStep(5)} />} + {step === 5 && } +
+ ); +}; + +export default MultiStepForm; diff --git a/src/components/NewBillboard/Step1.tsx b/_dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx similarity index 97% rename from src/components/NewBillboard/Step1.tsx rename to _dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx index 5901fcf..a976c8b 100644 --- a/src/components/NewBillboard/Step1.tsx +++ b/_dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx @@ -1,151 +1,151 @@ -"use client"; - -import RoundedButton from "@/components/elements/RoundedButton"; -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 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 { useTranslation } from "react-i18next"; - -const Step1 = ({ nextStep }: { nextStep: () => void }) => { - const { t } = useTranslation("common"); - const { formData, updateForm } = useBillboardForm(); - const [categories, setCategories] = useState([]); - const { request } = useAxios(); - - const validationSchema = useMemo( - () => - Yup.object({ - stateId: Yup.string().required(t("billboards.form.validation.provinceRequired")), - cityId: Yup.string().required(t("billboards.form.validation.cityRequired")), - 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")), - neighbourhood: Yup.string().required(t("billboards.form.validation.neighbourhoodRequired")), - address: Yup.string().required(t("billboards.form.validation.addressRequired")), - images: Yup.array() - .min(1, t("billboards.form.validation.minImages")) - .max(5, t("billboards.form.validation.maxImages")) - .required(t("billboards.form.validation.imagesRequired")), - markerCoordinate: Yup.array() - .of(Yup.number().required()) - .length(2, t("billboards.form.validation.mapLocationRequired")) - .required(t("billboards.form.validation.mapLocationRequired")), - }), - [t] - ); - - useEffect(() => { - const fetchData = async () => { - try { - const response = await request<{ categories: IAdvertisingCategory[] }>( - "GET", - "/advertising/categories" - ); - setCategories(response?.categories); - } catch (err) { - console.log(err); - } - }; - fetchData(); - }, [request]); - - const formik = useFormik({ - initialValues: { - cityId: formData.cityId || "", - stateId: formData.stateId || "", - adsTitle: formData.adsTitle || "", - categoryId: formData.categoryId || "", - images: formData.images || [], - address: formData.address || "", - neighbourhood: formData.neighbourhood || "", - description: formData.description || "", - markerCoordinate: formData.markerCoordinate || null, - }, - validationSchema, - onSubmit: (values) => { - updateForm(values); - nextStep(); - }, - }); - - return ( -
-
{t("billboards.createTitle")}
- formik.setFieldValue("categoryId", e.target.value)} - > - - {categories?.map((item: IAdvertisingCategory) => ( - - ))} - - {formik.touched.categoryId && formik.errors.categoryId && ( -

{formik.errors.categoryId}

- )} -
- - {formik.touched.adsTitle && formik.errors.adsTitle && ( -

{formik.errors.adsTitle}

- )} - - {formik.touched.address && formik.errors.address && ( -

{formik.errors.address}

- )} -
- - - {selectedLocation && ( - - location icon - - )} - - {formik.touched.markerCoordinate && formik.errors.markerCoordinate && ( -

- {formik.errors.markerCoordinate as string} -

- )} -
- - ); -}; - -export default LocationSelector; +/* eslint-disable @typescript-eslint/no-explicit-any */ +"use client"; + +import { ICity, IProvince } from "@/types/types"; +import SelectBox from "@/components/elements/SelectBox"; +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"; +import Image from "next/image"; +import { useBillboardForm } from "@/contexts/BillboardFormContext"; +import { useTranslation } from "react-i18next"; + +interface LocationSelectorProps { + formik: any; +} + +const LocationSelector: React.FC = ({ formik }) => { + const { t } = useTranslation("common"); + const { formData } = useBillboardForm(); + + const { request } = useAxios(); + const [allStates, setAllStates] = useState(null); + const [cities, setCities] = useState(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 fetchCities = async (provinceId: string) => { + try { + const response = await request<{ cities: ICity[] }>( + "GET", + `/cities/${provinceId}` + ); + setCities(response?.cities || []); + } catch (err) { + console.log(err); + } + }; + + 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]), + }); + } + }, [formik?.values?.markerCoordinate]); + + const handleProvinceChange = (e: React.ChangeEvent) => { + 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 ( + <> + + + {allStates?.map((item: IProvince) => ( + + ))} + + {formik.touched.stateId && formik.errors.stateId && ( + + {formik.errors.stateId} + + )} + + formik.setFieldValue("cityId", e.target.value)} + > + + {cities?.map((city: ICity) => ( + + ))} + + {formik.touched.cityId && formik.errors.cityId && ( + + {formik.errors.cityId} + + )} + + + {formik.touched.neighbourhood && formik.errors.neighbourhood && ( +

{formik.errors.neighbourhood}

+ )} + + {formik.touched.address && formik.errors.address && ( +

{formik.errors.address}

+ )} +
+ + + {selectedLocation && ( + + location icon + + )} + + {formik.touched.markerCoordinate && formik.errors.markerCoordinate && ( +

+ {formik.errors.markerCoordinate as string} +

+ )} +
+ + ); +}; + +export default LocationSelector; diff --git a/public/images/icons/activity.svg b/public/images/icons/activity.svg new file mode 100644 index 0000000..e689c87 --- /dev/null +++ b/public/images/icons/activity.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/archive-book.svg b/public/images/icons/archive-book.svg new file mode 100644 index 0000000..b4dfd2d --- /dev/null +++ b/public/images/icons/archive-book.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/images/icons/chart-square.svg b/public/images/icons/chart-square.svg new file mode 100644 index 0000000..8e92011 --- /dev/null +++ b/public/images/icons/chart-square.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/user-remove.svg b/public/images/icons/user-remove.svg new file mode 100644 index 0000000..b11d331 --- /dev/null +++ b/public/images/icons/user-remove.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx b/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx index be16f6a..ae02b7d 100644 --- a/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx +++ b/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx @@ -148,7 +148,7 @@ export default function CourseDetail({ setVideoFields(getAcademyCourses.data.courses); setSelectedVideo(getAcademyCourses.data.courses[0]) } catch (err) { - console.log("get error:", err); + console.error("get error:", err); toast.error(t("academy.course.loadError")); } finally { } @@ -164,20 +164,16 @@ export default function CourseDetail({ `/academy/course/checkCoursePurchase/${id}` // ارسال ID در مسیر ); - console.log(getCoursespayment); - if (getCoursespayment?.success) { if (getCoursespayment.isPurchased) { - console.log("دوره خریداری شده است"); setIsPurchased(true) } else { - console.log("دوره خریداری نشده است"); setIsPurchased(false) } } } catch (err) { - console.log("get error:", err); + console.error("get error:", err); toast.error(t("academy.course.loadError")); } }; @@ -363,7 +359,7 @@ export default function CourseDetail({ setCourses([]); } } catch (err) { - console.log("get error:", err); + console.error("get error:", err); toast.error(t("academy.course.loadError")); setCourses([]); } finally { @@ -484,13 +480,7 @@ export default function CourseDetail({ // نمایش لودینگ loadingToastId = toast.loading(t("academy.course.connectingPayment")); - - console.log("ارسال درخواست پرداخت:", { - planDuration: plan.duration, - planLabel: plan.label, - price: plan.price - }); - + const response = await request( "POST", "/academy/academy/course/payment-web", @@ -501,9 +491,7 @@ export default function CourseDetail({ // بستن لودینگ toast.dismiss(loadingToastId); - - console.log("پاسخ کامل سرور:", response); - + // پردازش پاسخ (ساختارهای مختلف احتمالی) let authority = null; let paymentUrl = null; @@ -521,7 +509,6 @@ export default function CourseDetail({ } if (paymentUrl) { - console.log("هدایت به درگاه:", paymentUrl); window.open(paymentUrl, "_blank", "noopener,noreferrer"); toast.success(t("academy.course.redirectedToPayment")); diff --git a/src/app/(projects)/projects/payment/success/page.tsx b/src/app/(projects)/projects/payment/success/page.tsx index fb0a464..be60789 100644 --- a/src/app/(projects)/projects/payment/success/page.tsx +++ b/src/app/(projects)/projects/payment/success/page.tsx @@ -18,10 +18,12 @@ function SuccessProject() { const { request } = useAxios(); const searchParams = useSearchParams(); const projectId = searchParams.get("projectId"); + const paymentType = searchParams.get("type"); const [project, setProject] = useState(); const [typeList, setTypeList] = useState(null); const [price, setPrice] = useState(""); + const [completionSubmitted, setCompletionSubmitted] = useState(false); const getDisplayTypeLabel = (projectType?: string) => { if (projectType === "normal" || projectType === "free") { @@ -54,8 +56,8 @@ function SuccessProject() { } }; void fetchAd(); - void fetchStates(); - }, [projectId, request]); + if (!paymentType) void fetchStates(); + }, [projectId, request, paymentType]); useEffect(() => { if (project?.project_type && typeList) { @@ -66,6 +68,38 @@ function SuccessProject() { } }, [project, typeList]); + // پرداخت مرحله تکمیل پروژه: نظر و امتیازی که قبل از رفتن به درگاه ذخیره شده بود حالا ثبت می‌شود + useEffect(() => { + if (paymentType !== "completion" || !projectId || completionSubmitted) return; + if (typeof window === "undefined") return; + + const key = `project-done-${projectId}`; + const stashed = sessionStorage.getItem(key); + if (!stashed) { + setCompletionSubmitted(true); + return; + } + + const submitDone = async () => { + try { + const { comment, rate, user_id } = JSON.parse(stashed); + const body: Record = { + project_id: projectId, + comment, + user_id, + }; + if (rate) body.rate = rate; + await request("POST", "/projects/done/web", body); + } catch (err) { + console.log(err); + } finally { + sessionStorage.removeItem(key); + setCompletionSubmitted(true); + } + }; + void submitDone(); + }, [paymentType, projectId, completionSubmitted, request]); + return ( @@ -84,10 +118,12 @@ function SuccessProject() {
{project && }
- - {getDisplayTypeLabel(project?.project_type)}:{" "} - {Number(price).toLocaleString()} {t("settings.toman")} - + {!paymentType && ( + + {getDisplayTypeLabel(project?.project_type)}:{" "} + {Number(price).toLocaleString()} {t("settings.toman")} + + )}

{t("projects.payment.reviewNotice")}

diff --git a/src/app/gift/[userId]/page.tsx b/src/app/gift/[userId]/page.tsx new file mode 100644 index 0000000..8c2abef --- /dev/null +++ b/src/app/gift/[userId]/page.tsx @@ -0,0 +1,179 @@ +"use client"; + +import Container from "@/components/elements/Container"; +import RoundedButton from "@/components/elements/RoundedButton"; +import RoundedInput from "@/components/elements/RoundedInput"; +import UserInfo from "@/components/main/UserInfo"; +import PageTitle from "@/components/settings/PageTitle"; +import useAxios from "@/hooks/useAxios"; +import { filterChipClass } from "@/lib/ui/buttonStyles"; +import { User } from "@/types/types"; +import React, { useEffect, useMemo, useState } from "react"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import { useTranslation } from "react-i18next"; + +const PRESET_AMOUNTS = [2000, 5000, 10000, 15000, 20000, 50000, 100000]; +const CUSTOM_MIN = 5000; +const CUSTOM_MAX = 100000000; + +interface GiftPageProps { + params: Promise<{ userId: string }>; +} + +function GiftPage({ params }: GiftPageProps) { + const { t } = useTranslation("common"); + const { userId } = React.use(params); + const { request } = useAxios(); + const router = useRouter(); + + const [userDetail, setUserDetail] = useState(); + const [selectedAmount, setSelectedAmount] = useState( + PRESET_AMOUNTS[0] + ); + const [customAmount, setCustomAmount] = useState(""); + const [isCustom, setIsCustom] = useState(false); + const [message, setMessage] = useState(""); + const [submitting, setSubmitting] = useState(false); + + useEffect(() => { + const fetchUser = async () => { + const response = await request<{ user: User }>( + "GET", + `/users/get?user_id=${userId}` + ); + setUserDetail(response?.user); + }; + void fetchUser(); + }, [userId, request]); + + const finalAmount = useMemo(() => { + if (isCustom) return Number(customAmount) || 0; + return selectedAmount || 0; + }, [isCustom, customAmount, selectedAmount]); + + const isCustomValid = + !isCustom || + (Number(customAmount) >= CUSTOM_MIN && Number(customAmount) <= CUSTOM_MAX); + + const canSubmit = finalAmount > 0 && isCustomValid && !submitting; + + const handleSubmit = async () => { + if (!canSubmit) return; + setSubmitting(true); + try { + const response = await request<{ authority?: string; paymentUrl?: string }>( + "POST", + "/gifts/initiate", + { receiverId: userId, amount: finalAmount, message } + ); + if (response?.paymentUrl) { + router.push(response.paymentUrl); + } else if (response?.authority) { + router.push(`https://www.zarinpal.com/pg/StartPay/${response.authority}`); + } else { + toast.error(t("giftPage.paymentInfoError")); + } + } catch (err: unknown) { + const message2 = + (err as { response?: { data?: { message?: string } } })?.response + ?.data?.message || t("giftPage.paymentStartError"); + toast.error(message2); + } finally { + setSubmitting(false); + } + }; + + return ( + +
+ {t("giftPage.title")} + + {userDetail ? ( +
+ +
+ ) : null} + +
+ {PRESET_AMOUNTS.map((amount) => ( + + ))} + +
+ + {isCustom ? ( +
+ setCustomAmount(e.target.value)} + /> + {!isCustomValid && customAmount ? ( +

+ {t("giftPage.customAmountInvalid", { + min: CUSTOM_MIN.toLocaleString(), + max: CUSTOM_MAX.toLocaleString(), + })} +

+ ) : null} +
+ ) : null} + +
+