From 200b3875041addd3a7f6c1a21e0b68c48dd34fd3 Mon Sep 17 00:00:00 2001 From: payacom Date: Thu, 24 Sep 2026 20:56:23 +0330 Subject: [PATCH] full app --- .claude/launch.json | 11 - .claude/worktrees/agent-af1dfc8fe9a6cb198 | 1 + .../MultiStepForm.tsx | 24 - .../Step1.tsx | 151 ---- .../Step2.tsx | 89 -- .../Step3.tsx | 176 ---- .../Step4.tsx | 193 ----- .../Step5.tsx | 268 ------ .../step1/ImageUploader.tsx | 90 --- .../step1/LocationSelector.tsx | 202 ----- next.config.ts | 14 +- public/images/og-image.png | Bin 0 -> 38740 bytes public/robots.txt | 7 +- public/service-worker.js | 15 +- src/api/fetchPostById.ts | 4 +- src/api/fetchPosts.ts | 2 - .../(register)/register/avatar/page.tsx | 177 ++++ .../(register)/register/complete/page.tsx | 21 +- .../(register)/register/fullname/page.tsx | 74 +- src/app/(auth)/(register)/register/page.tsx | 7 +- src/app/(auth)/verify/sizes/page.tsx | 2 +- .../academy/[id]/[title]/CourseClient.tsx | 238 ++---- .../(projects)/academy/[id]/[title]/page.tsx | 57 +- .../[academyId]/AcademyProfileClient.tsx | 13 +- .../(projects)/projects/[id]/[title]/page.tsx | 24 +- src/app/billboards/[id]/[title]/page.tsx | 53 +- src/app/billboards/profile/[...id]/page.tsx | 9 +- .../billboards/profile/[id]/[title]/page.tsx | 9 +- src/app/booking/[userId]/layout.tsx | 16 + src/app/gift/[userId]/page.tsx | 49 +- src/app/gift/layout.tsx | 7 + src/app/gift/payment/failed/page.tsx | 15 +- src/app/gift/payment/success/page.tsx | 19 +- src/app/layout.tsx | 10 +- src/app/new-post/page.tsx | 1 + src/app/not-found.tsx | 25 + src/app/offer/[id]/page.tsx | 136 ++-- src/app/offer/payment/failed/page.tsx | 20 +- src/app/offer/payment/success/page.tsx | 17 +- src/app/posts/[id]/[[...slug]]/page.tsx | 26 +- src/app/robots.ts | 3 + src/app/settings/academy/Components/Box.tsx | 2 +- .../academy/Components/OrdersCart.tsx | 2 +- src/app/settings/academy/my-courses/page.tsx | 10 +- src/app/settings/academy/wallet/page.tsx | 11 +- src/app/settings/ads/[id]/page.tsx | 135 ++++ src/app/settings/ads/new/page.tsx | 765 ++++++++++++++++++ src/app/settings/ads/page.tsx | 97 +++ src/app/settings/auto-direct/page.tsx | 58 ++ src/app/settings/booking/list/page.tsx | 9 +- .../settings/chats/[username]/[id]/page.tsx | 41 +- src/app/settings/chats/find-partner/page.tsx | 11 +- src/app/settings/chats/nearby/page.tsx | 9 +- src/app/settings/chats/page.tsx | 9 +- .../settings/chats/rooms/[roomId]/page.tsx | 5 +- src/app/settings/chats/rooms/page.tsx | 5 +- src/app/settings/edit/sizes/page.tsx | 2 +- src/app/settings/gifts/page.tsx | 9 +- src/app/settings/my-billboards/page.tsx | 9 +- src/app/settings/notifications/page.tsx | 9 +- src/app/settings/offers/page.tsx | 9 +- .../settings/profile/user-settings/page.tsx | 24 + src/app/settings/profile/visitors/page.tsx | 9 +- src/app/settings/shareable-links/page.tsx | 24 + src/app/settings/tickets/page.tsx | 9 +- src/app/settings/workroom/page.tsx | 9 +- src/app/shops/[shopId]/layout.tsx | 15 + .../[shopId]/products/[listingId]/page.tsx | 3 +- src/app/shops/checkout/[orderId]/page.tsx | 48 +- src/app/shops/discover/layout.tsx | 23 + .../[listingId]/ListingDetailClient.tsx | 16 +- src/app/shops/listing/[listingId]/page.tsx | 5 +- src/app/shops/profile/[shopId]/page.tsx | 6 +- src/app/sitemap.xml/route.ts | 12 + src/app/sitemaps/[type]/[[...page]]/route.ts | 6 + src/app/users/[username]/page.tsx | 15 +- src/components/NumberedStarRating.tsx | 52 ++ src/components/PushNotificationListener.tsx | 26 +- src/components/VpnWarningGate.tsx | 62 ++ src/components/academy/CommentsModal.tsx | 12 +- .../academy/CourseCommentsContent.tsx | 46 +- src/components/academy/InfinitePakage.tsx | 30 +- src/components/academy/MainModelCard.tsx | 30 +- .../academy/MainModelCardActions.tsx | 22 +- src/components/auth/AuthOtpInput.tsx | 18 + .../billboards/BillboardAdNotFound.tsx | 17 - .../billboards/BillboardNotFound.tsx | 17 - .../BillboardPage/BillboardDetails.tsx | 5 +- .../billboards/InfiniteBillboard.tsx | 12 + .../BillboardCommentsModal.tsx | 366 +++++---- .../NewBillboard/submitBillboard.ts | 5 + .../billboards/Profile/AdProfileContent.tsx | 6 +- .../booking/IncomingBookingsList.tsx | 9 +- src/components/chat/ChatMessageList.tsx | 9 +- src/components/chat/GiftInChatModal.tsx | 38 +- .../chat/GiftPaymentResultModal.tsx | 54 ++ src/components/chat/MessageList.tsx | 7 +- .../comments/CommentComposerBar.tsx | 43 + src/components/comments/CommentDrawer.tsx | 167 ++++ .../comments/NumberedStarRating.tsx | 53 ++ src/components/course/CourseForm.tsx | 57 +- src/components/course/CourseList.tsx | 9 +- src/components/course/CourseManager.tsx | 25 +- src/components/course/SubscriptionCard.tsx | 2 +- src/components/elements/EmptyState.tsx | 45 ++ src/components/main/UserInfo.tsx | 9 +- src/components/models/InfinitePosts.tsx | 9 +- .../models/MainModelCard/CommentsModal.tsx | 12 +- .../MainModelCard/MainModelCardActions.tsx | 2 +- .../models/ModelPage/ModelContent.tsx | 6 +- src/components/payment/WalletUsagePrompt.tsx | 67 ++ src/components/posts/PostCommentsContent.tsx | 33 +- src/components/posts/PostFeedView.tsx | 5 +- src/components/posts/PostLinkPicker.tsx | 114 +++ src/components/posts/ReelsImageCarousel.tsx | 7 +- src/components/posts/ReelsPostActions.tsx | 2 +- src/components/posts/ReelsPostCard.tsx | 142 +++- src/components/posts/ReelsPostOptionsMenu.tsx | 38 + src/components/profile/ReferralCapture.tsx | 68 ++ src/components/profile/ShareProfileModal.tsx | 64 +- src/components/projects/InfiniteProjects.tsx | 22 +- src/components/search/InfiniteSearch.tsx | 63 +- .../settings/AutoDirectFormModal.tsx | 188 +++++ src/components/settings/AutoDirectList.tsx | 110 +++ src/components/settings/FavoritesGrid.tsx | 9 +- src/components/settings/SettingsClient.tsx | 2 + .../settings/ShareableLinksManager.tsx | 307 +++++++ .../settings/profile/ProfileContent.tsx | 64 +- .../settings/settings/InviteFriends.tsx | 73 ++ src/components/shops/ShopCartTab.tsx | 9 +- .../shops/ShopProductCommentsModal.tsx | 68 +- src/components/stories/StoryRing.tsx | 10 +- src/components/ui/IdeaComponent.tsx | 2 +- src/components/ui/SocialStatButton.tsx | 2 +- src/config/seoConfig.ts | 2 +- src/config/site.ts | 2 +- src/contexts/BillboardFormContext.tsx | 2 +- src/lib/api/fetchApiJson.ts | 4 +- src/lib/auth/registrationRoutes.ts | 6 +- src/lib/buildAcademyListSeo.ts | 4 +- src/lib/buildCourseDetailSeo.ts | 52 +- src/lib/buildProjectsListSeo.ts | 4 +- src/lib/downloadFile.ts | 22 + src/lib/formatLikeDisplay.ts | 3 +- src/lib/i18n/seo.ts | 33 +- src/lib/i18n/server.ts | 12 +- src/lib/postSlug.ts | 47 +- src/lib/sitemap/builders.ts | 62 +- src/lib/sitemap/config.ts | 2 +- src/lib/ui/buttonStyles.ts | 7 +- src/locales/en/common.json | 172 +++- src/locales/fa/common.json | 172 +++- src/middleware.ts | 86 +- src/types/types.ts | 2 +- 154 files changed, 4984 insertions(+), 2169 deletions(-) delete mode 100644 .claude/launch.json create mode 160000 .claude/worktrees/agent-af1dfc8fe9a6cb198 delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/Step2.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/Step3.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/Step4.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/Step5.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/step1/ImageUploader.tsx delete mode 100644 _dead_code_quarantine/NewBillboard-duplicate-component/step1/LocationSelector.tsx create mode 100644 public/images/og-image.png create mode 100644 src/app/(auth)/(register)/register/avatar/page.tsx create mode 100644 src/app/booking/[userId]/layout.tsx create mode 100644 src/app/not-found.tsx create mode 100644 src/app/settings/ads/[id]/page.tsx create mode 100644 src/app/settings/ads/new/page.tsx create mode 100644 src/app/settings/ads/page.tsx create mode 100644 src/app/settings/auto-direct/page.tsx create mode 100644 src/app/settings/shareable-links/page.tsx create mode 100644 src/app/shops/[shopId]/layout.tsx create mode 100644 src/app/shops/discover/layout.tsx create mode 100644 src/components/NumberedStarRating.tsx create mode 100644 src/components/VpnWarningGate.tsx delete mode 100644 src/components/billboards/BillboardAdNotFound.tsx delete mode 100644 src/components/billboards/BillboardNotFound.tsx create mode 100644 src/components/chat/GiftPaymentResultModal.tsx create mode 100644 src/components/comments/CommentComposerBar.tsx create mode 100644 src/components/comments/CommentDrawer.tsx create mode 100644 src/components/comments/NumberedStarRating.tsx create mode 100644 src/components/elements/EmptyState.tsx create mode 100644 src/components/payment/WalletUsagePrompt.tsx create mode 100644 src/components/profile/ReferralCapture.tsx create mode 100644 src/components/settings/AutoDirectFormModal.tsx create mode 100644 src/components/settings/AutoDirectList.tsx create mode 100644 src/components/settings/ShareableLinksManager.tsx create mode 100644 src/components/settings/settings/InviteFriends.tsx create mode 100644 src/lib/downloadFile.ts diff --git a/.claude/launch.json b/.claude/launch.json deleted file mode 100644 index bc5aa17..0000000 --- a/.claude/launch.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "version": "0.0.1", - "configurations": [ - { - "name": "modstagram-next-dev", - "runtimeExecutable": "npm", - "runtimeArgs": ["run", "dev"], - "port": 3004 - } - ] -} diff --git a/.claude/worktrees/agent-af1dfc8fe9a6cb198 b/.claude/worktrees/agent-af1dfc8fe9a6cb198 new file mode 160000 index 0000000..3b89bbf --- /dev/null +++ b/.claude/worktrees/agent-af1dfc8fe9a6cb198 @@ -0,0 +1 @@ +Subproject commit 3b89bbfec327e50e9a1d5952f429864b48f058a3 diff --git a/_dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx b/_dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx deleted file mode 100644 index cad7483..0000000 --- a/_dead_code_quarantine/NewBillboard-duplicate-component/MultiStepForm.tsx +++ /dev/null @@ -1,24 +0,0 @@ -"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/_dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx b/_dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx deleted file mode 100644 index a976c8b..0000000 --- a/_dead_code_quarantine/NewBillboard-duplicate-component/Step1.tsx +++ /dev/null @@ -1,151 +0,0 @@ -"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; diff --git a/next.config.ts b/next.config.ts index 8152ad1..6d6b85f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -66,25 +66,31 @@ const nextConfig = { minimumCacheTTL: 31536000, }, - // ۳. ریدایرکت دائمی دامنه قدیمی .ir به دامنه اصلی .com + // ۳. ریدایرکت دائمی دامنه قدیمی .ir و نسخه بدون www به دامنه اصلی www.modstagram.com async redirects() { return [ { source: '/:path*', has: [{ type: 'host', value: 'modstagram.ir' }], - destination: 'https://modstagram.com/:path*', + destination: 'https://www.modstagram.com/:path*', permanent: true, }, { source: '/:path*', has: [{ type: 'host', value: 'www.modstagram.ir' }], - destination: 'https://modstagram.com/:path*', + destination: 'https://www.modstagram.com/:path*', permanent: true, }, { source: '/:path*', has: [{ type: 'host', value: 'app.modstagram.ir' }], - destination: 'https://modstagram.com/:path*', + destination: 'https://www.modstagram.com/:path*', + permanent: true, + }, + { + source: '/:path*', + has: [{ type: 'host', value: 'modstagram.com' }], + destination: 'https://www.modstagram.com/:path*', permanent: true, }, ]; diff --git a/public/images/og-image.png b/public/images/og-image.png new file mode 100644 index 0000000000000000000000000000000000000000..61220e4c6309fe4b7ec81a2d1190066776030140 GIT binary patch literal 38740 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 { const title = payload.notification?.title || "modstagram"; + // عکسِ فرستنده (یا بنرِ دستیِ ادمین برای admin_broadcast) — قبلاً همیشه لوگوی ثابتِ + // اپ بود، صرف‌نظر از اینکه نوتیف مربوط به کدوم کاربره؛ notification.image رو + // utils/pushNotification.js بک‌اند از قبل می‌فرسته (فقط این‌جا نادیده گرفته می‌شد) + const avatarOrBanner = payload.notification?.image; self.registration.showNotification(title, { body: payload.notification?.body, - icon: "/images/icons/192x192.png", + icon: avatarOrBanner || "/images/icons/192x192.png", badge: "/images/icons/192x192.png", + // بنرِ بزرگِ زیرِ متن (برای admin_broadcast که واقعاً قراره «بنر» باشه)، غیر از + // آیکونِ کوچیکِ گردِ کنارِ عنوان — مرورگرهایی که image رو ساپورت نکنن سرشون این رو + // نادیده می‌گیرن، پس بی‌خطره + image: avatarOrBanner || undefined, data: payload.data || {}, }); }); @@ -33,7 +41,10 @@ if (messaging) { self.addEventListener("notificationclick", (event) => { event.notification.close(); - const url = "/settings/notifications"; + // بک‌اند وقتی خودش دقیقاً می‌دونه مقصد کجاست (پیام چت، تگِ پست و...) یه مسیرِ نسبیِ + // آماده می‌فرسته (نگاه کن به utils/notifyUser.js/chatHelpers.js: data.link) — وگرنه + // مثلِ قبل می‌ریم صفحه‌ی عمومیِ اعلان‌ها + const url = event.notification.data?.link || "/settings/notifications"; event.waitUntil( self.clients.matchAll({ type: "window", includeUncontrolled: true }).then((clientsArr) => { const existing = clientsArr.find((c) => c.url.includes(url)); diff --git a/src/api/fetchPostById.ts b/src/api/fetchPostById.ts index fcdabb1..0e33d5e 100644 --- a/src/api/fetchPostById.ts +++ b/src/api/fetchPostById.ts @@ -15,7 +15,7 @@ export async function fetchPostById( try { const res = await fetch(primaryUrl, { - cache: "no-store", + next: { revalidate: 45 }, headers, }); if (res.ok) { @@ -35,7 +35,7 @@ export async function fetchPostById( for (const url of fallbackUrls) { try { const res = await fetch(url, { - cache: "no-store", + next: { revalidate: 45 }, headers, }); if (!res.ok) continue; diff --git a/src/api/fetchPosts.ts b/src/api/fetchPosts.ts index 3cccc7e..0b279a4 100644 --- a/src/api/fetchPosts.ts +++ b/src/api/fetchPosts.ts @@ -20,8 +20,6 @@ export async function fetchPosts( seedPostId?: string; sort?: "latest"; reelsTab?: "for_you" | "following" | "saved" | "near_me"; - lat?: string; - lng?: string; heightMin?: string; heightMax?: string; weightMin?: string; diff --git a/src/app/(auth)/(register)/register/avatar/page.tsx b/src/app/(auth)/(register)/register/avatar/page.tsx new file mode 100644 index 0000000..6df505d --- /dev/null +++ b/src/app/(auth)/(register)/register/avatar/page.tsx @@ -0,0 +1,177 @@ +"use client"; + +import React, { useEffect, useState, useCallback } from "react"; +import AuthPageLayout, { + AuthFormFooter, + AuthPageContent, +} from "@/components/auth/AuthPageLayout"; +import AuthNextButton from "@/components/auth/AuthNextButton"; +import { IMAGE_BASE_URL } from "@/components/main/BaseUrl"; +import useAxios from "@/hooks/useAxios"; +import { IProfileData } from "@/types/types"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import toast from "react-hot-toast"; +import Cropper from "react-easy-crop"; +import getCroppedImg from "@/helpers/cropImage"; +import { Area } from "react-easy-crop"; +import { useTranslation } from "react-i18next"; +import LocalePageShell from "@/components/i18n/LocalePageShell"; + +// عین verify/avatar/page.tsx (ویزارد تایید هویت)، با این تفاوت که این مرحله +// زودتر آورده شده: همین بعد از نام/نام‌خانوادگی (register/fullname) و قبل از +// صفحه‌ی انتخابِ «ادامه ثبت‌نام / ورود به برنامه» (register/complete) — چون عکس +// همین‌جا آپلود می‌شه، دیگه لازم نیست ویزارد تایید هویت دوباره همین قدم رو بپرسه +// (verify/avatar/page.tsx دیگه از fullname/complete لینک نمی‌شه) +function RegisterAvatarPage() { + const { t } = useTranslation("common"); + const router = useRouter(); + const { request } = useAxios(); + const [avatar, setAvatar] = useState(null); + const [crop, setCrop] = useState({ x: 0, y: 0 }); + const [zoom, setZoom] = useState(1); + const [loadingUpload, setLoadingUpload] = useState(false); + const [croppedAreaPixels, setCroppedAreaPixels] = useState(null); + + const mobile = + typeof window !== "undefined" ? localStorage.getItem("mobile") : null; + + const fetchData = async () => { + try { + const response = await request<{ user: IProfileData }>("GET", "/profile"); + setAvatar(response?.user?.profile_image || null); + } catch (err) { + console.log(err); + } + }; + + useEffect(() => { + fetchData(); + }, []); + + const selectImage = async (event: React.ChangeEvent) => { + if (event.target.files && event.target.files[0]) { + const file = event.target.files[0]; + const reader = new FileReader(); + reader.onload = (e) => { + const imageDataUrl = e.target?.result as string; + setAvatar(imageDataUrl); + }; + reader.readAsDataURL(file); + } + }; + + const onCropComplete = useCallback( + (_croppedArea: Area, croppedPixels: Area) => { + setCroppedAreaPixels(croppedPixels); + }, + [] + ); + + const createCroppedImage = async () => { + if (!avatar || !croppedAreaPixels) return null; + const croppedImage = await getCroppedImg(avatar, croppedAreaPixels); + return croppedImage; + }; + + const goNext = () => router.push("/register/complete"); + + const uploadImage = async () => { + if (!avatar) { + toast.error(t("auth.selectProfileImage")); + return; + } + setLoadingUpload(true); + const croppedBlob = await createCroppedImage(); + if (!croppedBlob) { + toast.error(t("auth.cropError")); + setLoadingUpload(false); + return; + } + + const formData = new FormData(); + formData.append("profile_image", croppedBlob, "avatar.jpg"); + formData.append("mobile", mobile as string); + + try { + await request("POST", "/verify/profile_image", formData, { + headers: { "Content-Type": "multipart/form-data" }, + }); + toast.success(t("auth.imageSavedSuccess")); + goNext(); + } catch (err) { + console.log("Upload error:", err); + toast.error(t("auth.imageUploadError")); + setLoadingUpload(false); + } + }; + + return ( + + + + + {t("auth.profileImageTitle")} + + +

+ {avatar ? ( + + ) : ( + + )} +
+ + + + + + + + + + + {t("auth.saveAndContinue")} + + + + + ); +} + +export default RegisterAvatarPage; diff --git a/src/app/(auth)/(register)/register/complete/page.tsx b/src/app/(auth)/(register)/register/complete/page.tsx index ffc3fa2..bf4edb2 100644 --- a/src/app/(auth)/(register)/register/complete/page.tsx +++ b/src/app/(auth)/(register)/register/complete/page.tsx @@ -14,6 +14,7 @@ import toast from "react-hot-toast"; import { getSafeRedirectPath } from "@/lib/auth/postLogin"; import { useTranslation } from "react-i18next"; import LocalePageShell from "@/components/i18n/LocalePageShell"; +import { IProfileData } from "@/types/types"; function RegisterCompletePage() { const { t } = useTranslation("common"); @@ -21,6 +22,22 @@ function RegisterCompletePage() { const { request, loading } = useAxios(); const [action, setAction] = useState<"app" | "continue" | null>(null); + // مطابق درخواست: عکس پروفایل حالا زودتر، توی خودِ صفحه‌ی register/avatar گرفته + // می‌شه (قبل از این صفحه) — پس دیگه لازم نیست ویزارد تایید هویت با verify/avatar + // شروع بشه؛ مستقیم می‌ره سراغ قدم بعدی (عین navigateHandler توی verify/avatar/page.tsx) + const goToVerifyWizard = async () => { + try { + const response = await request<{ user: IProfileData }>("GET", "/profile"); + if (response?.user?.user_type === "user") { + router.push("/verify/expertise"); + return; + } + } catch (err) { + console.log(err); + } + router.push("/verify/gender"); + }; + const finishRegistration = async (next: "app" | "continue") => { setAction(next); try { @@ -30,7 +47,7 @@ function RegisterCompletePage() { localStorage.setItem("usertype", "user"); if (next === "continue") { - router.push("/verify/avatar"); + await goToVerifyWizard(); } else { router.push(getSafeRedirectPath()); } @@ -59,7 +76,7 @@ function RegisterCompletePage() { finishRegistration("continue")} - className=" w-full sm:w-auto" + className="!border-[#16a34a] !text-[#16a34a] w-full sm:w-auto" disabled={loading} loading={loading && action === "continue"} > diff --git a/src/app/(auth)/(register)/register/fullname/page.tsx b/src/app/(auth)/(register)/register/fullname/page.tsx index f1673db..3ce0c61 100644 --- a/src/app/(auth)/(register)/register/fullname/page.tsx +++ b/src/app/(auth)/(register)/register/fullname/page.tsx @@ -14,7 +14,6 @@ import * as yup from "yup"; import { useRouter } from "next/navigation"; import AuthNextButton from "@/components/auth/AuthNextButton"; import toast from "react-hot-toast"; -import { getSafeRedirectPath } from "@/lib/auth/postLogin"; import { useTranslation } from "react-i18next"; import LocalePageShell from "@/components/i18n/LocalePageShell"; import Image from "next/image"; @@ -32,9 +31,6 @@ function FullNamePage() { const router = useRouter(); const { request, loading } = useAxios(); const [nameSaved, setNameSaved] = useState(false); - const [choiceLoading, setChoiceLoading] = useState<"app" | "continue" | null>( - null - ); const [avatar, setAvatar] = useState(null); const isInstagramProvider = @@ -68,25 +64,18 @@ function FullNamePage() { [t] ); - const finishRegistration = async (action: "app" | "continue") => { - setChoiceLoading(action); + // فقط برای ثبت‌نامِ اینستاگرام صدا زده می‌شه (پایین‌تر) — چون اون کاربرها از قبل + // عکس دارن، همون ویزارد تایید هویت (verify/avatar) رو می‌بینن، نه صفحه‌ی جدیدِ + // register/avatar. بقیه‌ی کاربرها بعد از ثبت نام مستقیم به register/avatar می‌رن + const finishRegistrationInstagram = async () => { try { - await request("POST", "/register/user_type", { - user_type: "user", - }); + await request("POST", "/register/user_type", { user_type: "user" }); localStorage.setItem("usertype", "user"); - - if (action === "continue") { - router.push("/verify/avatar"); - } else { - router.push(getSafeRedirectPath()); - } + router.push("/verify/avatar"); } catch (err: any) { toast.error( err?.response?.data?.message || t("auth.registrationFinishFailed") ); - } finally { - setChoiceLoading(null); } }; @@ -117,7 +106,11 @@ function FullNamePage() { // no manual "enter app or continue" choice, matching the phone/Google flow's // choice screen being skipped only for this provider. if (isInstagramProvider) { - await finishRegistration("continue"); + await finishRegistrationInstagram(); + } else { + // بقیه‌ی کاربرها: به‌جای دکمه‌های «ادامه/ورود به برنامه» زیر همین فرم، + // اول مرحله‌ی آپلود عکس پروفایل میاد + router.push("/register/avatar"); } } catch (err: any) { toast.error(err?.response?.data?.message || t("auth.saveNameFailed")); @@ -196,48 +189,17 @@ function FullNamePage() { {formik.errors.family} )} - - {nameSaved && ( -

- {isInstagramProvider - ? t("auth.instagramContinuingToProfile") - : t("auth.nameSavedChoose")} -

- )} - {!nameSaved ? ( - - {t("auth.saveAndContinue")} - - ) : !isInstagramProvider ? ( -
- finishRegistration("continue")} - className=" w-full sm:w-auto" - disabled={Boolean(choiceLoading)} - loading={choiceLoading === "continue"} - > - {t("auth.continueRegistration")} - - finishRegistration("app")} - className="!border-[#FF0000] !text-[#FF0000] w-full sm:w-auto" - disabled={Boolean(choiceLoading)} - loading={choiceLoading === "app"} - > - {t("auth.enterApp")} - -
- ) : null} + + {t("auth.saveAndContinue")} +
diff --git a/src/app/(auth)/(register)/register/page.tsx b/src/app/(auth)/(register)/register/page.tsx index 040784a..b1db32f 100644 --- a/src/app/(auth)/(register)/register/page.tsx +++ b/src/app/(auth)/(register)/register/page.tsx @@ -46,12 +46,17 @@ function Register() { validationSchema: schema, onSubmit: async (values) => { const mobile = normalizeMobileInput(values.mobile); + // سیستم «معرفی به دوستان»: اگه کاربر قبل از این از یه لینکِ معرفی + // (/users/username?ref=CODE) اومده، ReferralCapture.tsx همین‌جا توی + // localStorage نگهش داشته — بک‌اند (registerController.js) این کد رو به + // referred_by وصل می‌کنه، پاداش بعد از تکمیلِ کاملِ ثبت‌نام پرداخت می‌شه + const referralCode = localStorage.getItem("referral_code"); try { await request( "POST", "/register", - { mobile }, + { mobile, ...(referralCode ? { referral_code: referralCode } : {}) }, { noToast: true } ); localStorage.setItem("mobile", mobile); diff --git a/src/app/(auth)/verify/sizes/page.tsx b/src/app/(auth)/verify/sizes/page.tsx index fc0389e..fbfb35f 100644 --- a/src/app/(auth)/verify/sizes/page.tsx +++ b/src/app/(auth)/verify/sizes/page.tsx @@ -101,7 +101,7 @@ function Sizes() { className={`p-1 rounded-2xl w-1/3 border text-sm flex items-center justify-center gap-1 ${ selectedButton === 1 ? "btn-modern--selected text-white" - : "bg-tertiary-light dark:bg-tertiary-dark text-black border-gray-400" + : "bg-tertiary-light dark:bg-tertiary-dark border-gray-400" }`} onClick={() => handleButtonPress(1)} > diff --git a/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx b/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx index 0e40ceb..796283b 100644 --- a/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx +++ b/src/app/(projects)/academy/[id]/[title]/CourseClient.tsx @@ -30,6 +30,7 @@ import { usePathname } from "next/navigation"; import Cookies from "js-cookie"; import { trackExploreInteraction } from "@/api/trackExploreInteraction"; import BillboardContactInfoModal from "@/components/billboards/BillboardPage/BillboardContactInfoModal"; +import WalletUsagePrompt from "@/components/payment/WalletUsagePrompt"; import { AcademyCourseDetailSkeleton } from "@/components/academy/AcademySkeletons"; import { useTranslation } from "react-i18next"; import BackButton from "@/components/ui/BackButton"; @@ -85,17 +86,28 @@ interface CourseDetailProps { initialIsPurchased?: boolean; } -interface MediaFile { - id: string; - title: string; - file: File | null; - preview?: string; +/** شکلِ واقعیِ ویدیوهایی که از /academy/academy/course/content میان و توی + * لیستِ سرفصل‌ها + پلیر رندر می‌شن (نه فرم‌آپلود — MOCK_VIDEOS[0] هم همین شکله) */ +interface CourseVideo { + _id: string; + file_name: string; course_video: string; - type: "image" | "video"; - isFree?: boolean; - is_free?: boolean; + is_free: boolean; + type: string; + duration: string; } +/** این اندپوینت‌ها ساختارهای مختلفی برمی‌گردونن (بعضاً حتی خودِ کد چند شکلِ + * احتمالی رو با || پشتِ‌سرِ‌هم چک می‌کنه) — به‌جای برگردوندنِ generic هرکدوم به + * یه interface دقیقِ جدا (که نیاز به rewrite بک‌اند برای یکدست‌کردن داشت)، یه + * تایپِ عمومیِ صریح که همون فیلدهایی که کد واقعاً استفاده می‌کنه رو پوشش می‌ده */ +type ApiResponse = { + success?: boolean; + message?: string; + data?: any; + [key: string]: any; +}; + export default function CourseDetail({ initialIsPurchased = false, }: CourseDetailProps) { @@ -105,6 +117,8 @@ export default function CourseDetail({ const [purchasing, setPurchasing] = useState(false); const [showContactInfoModal, setShowContactInfoModal] = useState(false); const [course] = useState(MOCK_COURSE); + const [walletBalance, setWalletBalance] = useState(0); + const [showWalletPrompt, setShowWalletPrompt] = useState(false); const pathname = usePathname(); const match = pathname.match(/\/academy\/([^\/]+)/); @@ -116,6 +130,13 @@ export default function CourseDetail({ const _id = courses[0]?._id; + useEffect(() => { + request<{ charged?: { available?: number } }>("GET", "/wallet/summary") + .then((res) => setWalletBalance(res?.charged?.available || 0)) + .catch(() => setWalletBalance(0)); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + const [comments, setComments] = useState([]); const [totalComments, setTotalComments] = useState(0); const [averageRate, setAverageRate] = useState(0); @@ -126,7 +147,7 @@ export default function CourseDetail({ const [sortBy, setSortBy] = useState("createdAt"); const [sortOrder, setSortOrder] = useState("desc"); const [usersCache, setUsersCache] = useState>({}); - const [videoFields, setVideoFields] = useState([]); + const [videoFields, setVideoFields] = useState([]); const [rating, setRating] = useState(0); @@ -138,11 +159,11 @@ export default function CourseDetail({ try { - const getAcademyCourses = (await request( + const getAcademyCourses = await request( "GET", `/academy/academy/course/content?courseId=${id}&page=1&limit=20`, {} - )) as MediaFile; + ); setVideoFields(getAcademyCourses.data.courses); setSelectedVideo(getAcademyCourses.data.courses[0]) @@ -158,7 +179,7 @@ export default function CourseDetail({ useEffect(() => { const getAcademyCourses = async () => { try { - const getCoursespayment = await request( + const getCoursespayment = await request( "GET", // تغییر به GET `/academy/course/checkCoursePurchase/${id}` // ارسال ID در مسیر ); @@ -187,7 +208,7 @@ export default function CourseDetail({ } try { - const response = await request("GET", `/profile/${userId}`); + const response = await request("GET", `/profile/${userId}`); if (response?.user) { const userData = response.user; setUsersCache((prev) => ({ ...prev, [userId]: userData })); @@ -204,7 +225,7 @@ export default function CourseDetail({ if (!_id) return; try { - const response = await request( + const response = await request( "GET", `/academy/course/${_id}/comments/count` ); @@ -223,15 +244,15 @@ export default function CourseDetail({ setLoading(true); try { - const response = await request( + const response = await request( "GET", `/academy/course/${_id}/comments?page=${pageNum}&limit=20&status=${statusFilter}&sortBy=${sortBy}&sortOrder=${sortOrder}` ); let sum = 0; let count = 0; - - response.data.stats.rateDistribution.forEach(element => { + + response.data.stats.rateDistribution.forEach((element: number) => { sum += Number(element); count++; }); @@ -242,7 +263,7 @@ export default function CourseDetail({ setRating(roundedRating); if (response?.success) { - let newComments = response.data.comments; + let newComments: any[] = response.data.comments; const pagination = response.data.pagination; if (newComments && newComments.length > 0) { @@ -340,7 +361,7 @@ export default function CourseDetail({ const getAcademyCourses = async () => { setCourseLoading(true); try { - const response = await request( + const response = await request( "GET", `/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}` ); @@ -387,46 +408,7 @@ export default function CourseDetail({ // eslint-disable-next-line react-hooks/exhaustive-deps }, [courses[0]?._id]); - // خرید دوره - // const handlePurchase = async () => { - // setPurchasing(true); - - // try { - // const response = await request( - // "POST", - // "/academy/academy/course/payment-web", - // { - // courseId: id, // یا _id که از courses[0]?._id می‌آید - // } - // ); - - // console.log("پاسخ کامل سرور:", response); - - // // بررسی موفقیت آمیز بودن درخواست - // if (response?.success && response?.paymentUrl) { - // // هدایت کاربر به درگاه پرداخت زرین‌پال - // window.location.href = response.paymentUrl; - // } else { - // // نمایش خطا به کاربر - // toast.error(response?.message || "خطا در اتصال به درگاه پرداخت"); - // setPurchasing(false); - // } - // } catch (error) { - // console.error("خطا در خرید:", error); - // toast.error("خطا در ارتباط با سرور"); - // setPurchasing(false); - // } - // }; - - - const handleBuyCourse = async () => { - const token = Cookies.get("token"); - - if (!token) { - toast.error(t("academy.course.loginToBuy")); - return; - } - + const startPayment = async (useWallet: boolean) => { setPurchasing(true); const loadingToastId = toast.loading( courses[0]?.is_free @@ -435,8 +417,9 @@ export default function CourseDetail({ ); try { - const response = await request("POST", "/academy/academy/course/payment-web", { + const response = await request("POST", "/academy/academy/course/payment-web", { courseId: id, + useWallet, }); toast.dismiss(loadingToastId); @@ -447,6 +430,12 @@ export default function CourseDetail({ return; } + if (response?.paidFromWallet) { + setIsPurchased(true); + toast.success(t("wallet.paidFromWalletSuccess")); + return; + } + const paymentUrl = response?.paymentUrl || response?.data?.paymentUrl || @@ -475,98 +464,23 @@ export default function CourseDetail({ } }; - const handlePurchase = async (plan: { - duration: string; - label: string; - price: number; - discount?: number; - }) => { - let loadingToastId = null; - - try { + // عین صفحه‌ی درخواست همکاری/سفارش فروشگاه: قبل پرداخت، اگه موجودی کیف پول داره + // بپرسه استفاده کنه یا نه + const handleBuyCourse = async () => { + const token = Cookies.get("token"); - - const token = Cookies.get("token"); - - if (!token) { - toast.error(t("academy.course.loginToBuy")); - return - } - - // نمایش لودینگ - loadingToastId = toast.loading(t("academy.course.connectingPayment")); - - const response = await request( - "POST", - "/academy/academy/course/payment-web", - { - courseId: id, // یا _id که از courses[0]?._id می‌آید - } - ); - - // بستن لودینگ - toast.dismiss(loadingToastId); - - // پردازش پاسخ (ساختارهای مختلف احتمالی) - let authority = null; - let paymentUrl = null; - - if (response?.data?.authority) { - authority = response.data.authority; - paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`; - } else if (response?.authority) { - authority = response.authority; - paymentUrl = `https://www.zarinpal.com/pg/StartPay/${authority}`; - } else if (response?.data?.paymentUrl) { - paymentUrl = response.data.paymentUrl; - } else if (response?.paymentUrl) { - paymentUrl = response.paymentUrl; - } - - if (paymentUrl) { - // window.open بعدِ یه await در سافاری (دسکتاپ و خصوصاً iOS) پاپ‌آپ‌بلاک می‌شه — - // چون دیگه synchronously به همون کلیکِ کاربر متصل نیست. window.location.href هم - // مطمئن‌تره، هم با بقیه‌ی مسیرهای پرداختِ این اپ (booking، پروژه، بیلبورد) یکی می‌مونه - window.location.href = paymentUrl; - return; - } else { - console.error("ساختار پاسخ نامعتبر:", response); - toast.error( - response?.data?.message || - response?.message || - t("academy.course.paymentStartError") - ); - } - - } catch (err: any) { - // بستن لودینگ در صورت خطا - if (loadingToastId) { - toast.dismiss(loadingToastId); - } - - console.error("خطا در شروع پرداخت:", err); - - // استخراج پیام خطا - let errorMessage = t("academy.course.paymentStartError"); - - if (err?.response?.data?.message) { - errorMessage = err.response.data.message; - } else if (err?.response?.data?.error) { - errorMessage = err.response.data.error; - } else if (err?.message) { - errorMessage = err.message; - } - - if (errorMessage.includes("amount") || errorMessage.includes("مبلغ")) { - errorMessage = t("academy.course.invalidAmount"); - } else if (errorMessage.includes("merchant")) { - errorMessage = t("academy.course.gatewayConfigError"); - } - - toast.error(errorMessage); + if (!token) { + toast.error(t("academy.course.loginToBuy")); + return; } - }; + if (!courses[0]?.is_free && walletBalance > 0) { + setShowWalletPrompt(true); + return; + } + + await startPayment(false); + }; // پخش ویدیو const handlePlayVideo = (video: (typeof MOCK_VIDEOS)[0]) => { @@ -591,7 +505,7 @@ export default function CourseDetail({ const finalPrice = courses[0]?.is_free ? 0 : courses[0]?.offerNumber > 0 - ? courses[0]?.price * (1 - courses[0]?.offerNumber / 100) + ? Number(courses[0]?.price) * (1 - courses[0]?.offerNumber / 100) : course.price; const courseContactInfo = courses[0]?.contactInfo; @@ -756,8 +670,8 @@ useEffect(() => { {/* آواتار کاربر */}
{ {/* اطلاعات کاربر و امتیاز */}
- {item?.user_id?.user_name || t("common.user")} - + {(typeof item?.user_id === "object" ? item.user_id?.user_name : undefined) || t("common.user")} + {item?.rate > 0 && ( @@ -855,12 +769,12 @@ useEffect(() => {
{Math.round( - courses[0]?.price * (1 - courses[0]?.offerNumber / 100) + Number(courses[0]?.price) * (1 - courses[0]?.offerNumber / 100) ).toLocaleString()}{" "} {t("settings.toman")} - {Math.round(courses[0]?.price).toLocaleString()}{" "} + {Math.round(Number(courses[0]?.price)).toLocaleString()}{" "} {t("settings.toman")} @@ -873,7 +787,7 @@ useEffect(() => {
) : ( - {Math.round(courses[0]?.price || course.price).toLocaleString()}{" "} + {Math.round(Number(courses[0]?.price) || course.price).toLocaleString()}{" "} {t("settings.toman")} )} @@ -1046,6 +960,16 @@ useEffect(() => { }} /> )} + + { + setShowWalletPrompt(false); + void startPayment(useWallet); + }} + />
); } \ No newline at end of file diff --git a/src/app/(projects)/academy/[id]/[title]/page.tsx b/src/app/(projects)/academy/[id]/[title]/page.tsx index 5de05ad..c8bf2ba 100644 --- a/src/app/(projects)/academy/[id]/[title]/page.tsx +++ b/src/app/(projects)/academy/[id]/[title]/page.tsx @@ -1,8 +1,11 @@ import { Metadata } from "next"; +import { cache } from "react"; +import { notFound } from "next/navigation"; import CourseDetail from "./CourseClient"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; import { getApiBaseUrl } from "@/components/main/BaseUrl"; -import { buildCourseDetailSeo } from "@/lib/buildCourseDetailSeo"; +import { buildCourseDetailSeo, buildCourseJsonLd } from "@/lib/buildCourseDetailSeo"; +import { safeJsonLdString } from "@/lib/seo/safeJsonLd"; import { getServerLanguage } from "@/lib/i18n/server"; import { translateCommon } from "@/lib/i18n/translate"; @@ -10,20 +13,33 @@ type Props = { params: Promise<{ id: string; title: string }>; }; +// Deduped per request: generateMetadata() and the page body's JSON-LD both need +// this course — cache() avoids firing the fetch to the backend twice. +const loadCourse = cache(async (id: string) => { + try { + const res = await fetch( + `${getApiBaseUrl()}/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}`, + { next: { revalidate: 45 } } + ); + + const response = await res.json(); + return ( + response?.data?.courses?.[0] || + response?.courses?.[0] || + response?.[0] || + null + ); + } catch (error) { + console.error("خطا در دریافت اطلاعات دوره:", error); + return null; + } +}); + export async function generateMetadata({ params }: Props): Promise { const { id, title: slugTitle } = await params; try { - const res = await fetch( - `${getApiBaseUrl()}/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}`, - { cache: "no-store" } - ); - - const response = await res.json(); - const course = - response?.data?.courses?.[0] || - response?.courses?.[0] || - response?.[0]; + const course = await loadCourse(id); if (course) { const lang = await getServerLanguage(); @@ -53,5 +69,22 @@ export async function generateMetadata({ params }: Props): Promise { export default async function CoursePage({ params }: Props) { const { id } = await params; - return ; + const lang = await getServerLanguage(); + const course = await loadCourse(id); + + if (!course) notFound(); + + return ( + <> + {course && ( +