From af5b27b610745f46d1e934dd0cb267bd855738e1 Mon Sep 17 00:00:00 2001 From: payacom <59262811+payacom@users.noreply.github.com> Date: Sat, 18 Jul 2026 14:29:05 +0330 Subject: [PATCH] full update --- public/favicon.ico | Bin 0 -> 11537 bytes public/images/icons/192x192.png | Bin 0 -> 11045 bytes public/images/icons/actions-toggle.svg | 6 + public/images/icons/apple-touch-icon.png | Bin 0 -> 11045 bytes public/images/icons/bookmark-post.svg | 3 + public/images/icons/collaboration-request.svg | 6 + public/images/icons/follow-user.svg | 3 + public/images/icons/send-post.svg | 3 + public/images/icons/star-pack/star.svg | 11 + public/manifest.json | 34 +- public/service-worker.js | 55 +- public/sitemap-0.xml | 50 +- src/api/fetchBookmarkPosts.ts | 32 + src/api/fetchPosts.ts | 26 +- src/api/fetchProject.ts | 23 + src/api/fetchStories.ts | 63 ++ .../(login)/login-with-username/page.tsx | 9 +- src/app/(auth)/(login)/login/page.tsx | 2 + src/app/(auth)/(login)/verify-otp/page.tsx | 27 +- .../(auth)/(register)/register-otp/page.tsx | 20 +- .../(register)/register/complete/page.tsx | 2 +- .../(register)/register/fullname/page.tsx | 2 +- src/app/(auth)/AuthProviders.tsx | 7 +- src/app/(auth)/layout.tsx | 1 - src/app/(auth)/verify/colors/page.tsx | 22 +- .../(auth)/verify/cooperation-type/page.tsx | 4 +- src/app/(auth)/verify/gender/page.tsx | 2 +- src/app/(auth)/verify/sizes/page.tsx | 18 +- .../(projects)/academy/[[...slug]]/page.tsx | 109 +-- .../academy/[id]/[title]/CourseClient.tsx | 133 +++- .../(projects)/academy/[id]/[title]/page.tsx | 23 +- .../academy/payment/failed/page.tsx | 2 +- .../academy/payment/success/page.tsx | 2 +- .../[academyId]/AcademyProfileClient.tsx | 333 ++++++++++ .../academy/profile/[academyId]/page.tsx | 412 ++---------- .../(projects)/projects/[id]/[title]/page.tsx | 21 +- src/app/(projects)/projects/page.tsx | 60 +- src/app/[[...slug]]/page.tsx | 35 +- src/app/billboards/new/[id]/page.tsx | 3 +- src/app/billboards/payment/failed/page.tsx | 2 +- src/app/billboards/payment/success/page.tsx | 2 +- src/app/explore/[id]/ExploreReelClient.tsx | 54 ++ src/app/explore/[id]/page.tsx | 70 +- src/app/explore/page.tsx | 30 +- src/app/globals.css | 115 ++++ src/app/icon.png | Bin 0 -> 11537 bytes src/app/layout.tsx | 13 +- src/app/offer/[id]/page.tsx | 9 +- src/app/offer/payment/failed/page.tsx | 2 +- src/app/offer/payment/success/page.tsx | 4 +- src/app/posts/[id]/[[...slug]]/page.tsx | 98 +++ src/app/posts/[id]/page.tsx | 69 -- src/app/settings/academy/my-courses/page.tsx | 14 +- .../settings/academy/storeProfile/page.tsx | 11 +- src/app/settings/academy/wallet/page.tsx | 10 +- .../settings/chats/[username]/[id]/page.tsx | 38 +- src/app/settings/chats/layout.tsx | 12 + src/app/settings/chats/page.tsx | 10 +- src/app/settings/edit/colors/page.tsx | 4 +- .../settings/edit/cooperation-type/page.tsx | 4 +- src/app/settings/edit/layout.tsx | 12 + .../settings/edit/public-relations/page.tsx | 4 +- src/app/settings/edit/sizes/page.tsx | 8 +- src/app/settings/favorites/page.tsx | 20 + src/app/settings/financial/layout.tsx | 12 + src/app/settings/layout.tsx | 3 +- src/app/settings/my-billboards/layout.tsx | 12 + src/app/settings/notifications/layout.tsx | 12 + src/app/settings/notifications/page.tsx | 53 +- src/app/settings/offers/layout.tsx | 12 + src/app/settings/offers/page.tsx | 12 +- src/app/settings/page.tsx | 8 + src/app/settings/profile/ProfileClient.tsx | 46 ++ src/app/settings/profile/info/page.tsx | 6 +- src/app/settings/profile/layout.tsx | 28 +- src/app/settings/profile/page.tsx | 93 ++- .../settings/profile/user-settings/layout.tsx | 12 + .../settings/profile/user-settings/page.tsx | 144 ++++ src/app/settings/profile/visitors/layout.tsx | 12 + src/app/settings/profile/visitors/page.tsx | 88 +++ src/app/settings/tickets/layout.tsx | 12 + src/app/settings/workroom/layout.tsx | 12 + src/app/sitemap.ts | 126 ++-- src/app/storage/[...path]/route.ts | 2 +- src/app/users/[username]/page.tsx | 115 ++-- src/components/Layout.tsx | 4 + src/components/NewBillboard/Step1.tsx | 2 +- src/components/NewBillboard/Step2.tsx | 2 +- src/components/NewBillboard/Step3.tsx | 2 +- src/components/NewBillboard/Step4.tsx | 2 +- src/components/NewBillboard/Step5.tsx | 7 +- src/components/TabNavigation.tsx | 16 +- src/components/academy/AcademyFilter.tsx | 380 ++++------- src/components/academy/AcademyFilterModal.tsx | 199 ++++++ src/components/academy/AcademyPackageItem.tsx | 59 ++ src/components/academy/AcademySkeletons.tsx | 96 +++ src/components/academy/CommentsModal.tsx | 8 +- src/components/academy/InfinitePakage.tsx | 27 +- src/components/academy/MainModelCard.tsx | 54 +- .../academy/MainModelCardActions.tsx | 94 ++- src/components/auth/AuthButton.tsx | 4 +- src/components/auth/AuthNextButton.tsx | 8 +- src/components/auth/AuthSessionSync.tsx | 13 + src/components/auth/ExpertisePicker.tsx | 23 +- src/components/auth/RegisterRouteGuard.tsx | 120 ++++ src/components/auth/UsernameSuggestions.tsx | 4 +- .../billboards/BillboardPage/FilterModal.tsx | 29 +- .../BillboardLocationModal.tsx | 19 +- .../MainBillboardCardActions.tsx | 77 +-- .../billboards/NewBillboard/Step1.tsx | 2 +- .../billboards/NewBillboard/Step2.tsx | 2 +- .../billboards/NewBillboard/Step3.tsx | 2 +- .../billboards/NewBillboard/Step4.tsx | 2 +- .../billboards/NewBillboard/Step5.tsx | 7 +- .../Profile/AdProfileContentPosts.tsx | 3 +- .../Profile/AdProfileHeadRowTwo.tsx | 2 - src/components/chat/ChatGuideModal.tsx | 72 ++ src/components/chat/ChatHeader.tsx | 9 +- src/components/chat/ChatHeaderMenu.tsx | 16 +- src/components/chat/ChatMessageCard.tsx | 69 +- src/components/chat/ChatMessageList.tsx | 29 + src/components/chat/ForwardMessageModal.tsx | 4 +- src/components/chat/MediaPreviewModal.tsx | 124 ++++ src/components/chat/MessageInput.tsx | 17 +- src/components/chat/MultiImageModal.tsx | 24 +- src/components/chat/SharedPostBubble.tsx | 68 ++ src/components/chat/SharedStoryBubble.tsx | 92 +++ src/components/course/CourseForm.tsx | 201 +++++- src/components/course/CourseManager.tsx | 69 +- src/components/course/CustomDialog.tsx | 4 +- src/components/elements/Container.tsx | 12 +- src/components/elements/Modal.tsx | 26 +- src/components/elements/RoundedButton.tsx | 30 +- src/components/elements/RoundedDiv.tsx | 10 +- src/components/explore/ExploreAcademyCard.tsx | 3 +- src/components/explore/ExploreFilterBar.tsx | 47 +- src/components/explore/ExploreGrid.tsx | 109 ++- src/components/explore/ExploreReelsView.tsx | 166 +++-- src/components/explore/ExploreVideoThumb.tsx | 98 +++ src/components/explore/ReelsAcademyCard.tsx | 126 +++- src/components/main/BaseUrl.ts | 26 +- src/components/main/SharePostModal.tsx | 6 +- src/components/main/VerificationBadge.tsx | 5 +- .../models/AppearanceColorSelect.tsx | 107 +++ src/components/models/FilterModal.tsx | 260 +++++++- src/components/models/InfinitePosts.tsx | 18 +- .../models/MainModelCard/CommentsModal.tsx | 176 +---- .../models/MainModelCard/MainModelCard.tsx | 36 +- .../MainModelCard/MainModelCardActions.tsx | 218 +++--- src/components/models/ModelDetailModal.tsx | 2 +- .../models/ModelPage/LocationModal.tsx | 11 +- .../models/ModelPage/ModelContent.tsx | 277 ++++++-- .../models/ModelPage/ModelContentPosts.tsx | 5 +- src/components/models/ModelPage/ModelHead.tsx | 75 +-- .../models/ModelPage/ModelHeadRowTwo.tsx | 9 +- .../models/ModelPage/ProfileUserListModal.tsx | 134 ++++ src/components/models/ModelServiceModal.tsx | 2 +- src/components/models/ModelsFilter.tsx | 218 +++--- src/components/posts/CaptionWithMentions.tsx | 57 ++ .../posts/FollowingSuggestionsCarousel.tsx | 348 ++++++++++ src/components/posts/PostCommentsContent.tsx | 337 ++++++++++ src/components/posts/PostFeedView.tsx | 624 +++++++++++++----- src/components/posts/ReelsImageCarousel.tsx | 167 +++++ src/components/posts/ReelsPostActions.tsx | 473 +++++++------ src/components/posts/ReelsPostCard.tsx | 538 +++++++++++---- src/components/posts/ReelsScrollSlot.tsx | 34 + src/components/posts/ReelsTabBar.tsx | 61 ++ src/components/posts/SendPostModal.tsx | 214 ++++++ src/components/posts/TagUsersPicker.tsx | 159 +++-- src/components/projects/FilterModal.tsx | 252 +++++-- src/components/projects/NewProject/Step1.tsx | 2 +- src/components/projects/NewProject/Step2.tsx | 2 +- src/components/projects/NewProject/Step3.tsx | 7 +- .../NewProject/step1/AgeSelection.tsx | 7 +- .../NewProject/step1/ExpertiseSelector.tsx | 25 +- .../NewProject/step1/GenderSelection.tsx | 18 +- .../NewProject/step1/PublicTypeSelection.tsx | 7 +- src/components/projects/ProjectsFilter.tsx | 132 ++-- .../Workroom/ProjectRequestsAction.tsx | 2 +- .../Workroom/ProjectWorkroomActions.tsx | 4 +- src/components/pwa/PwaInstallPrompt.tsx | 100 +++ src/components/search/SearchCard.tsx | 10 +- src/components/search/SearchFilter.tsx | 6 + src/components/settings/FavoritesGrid.tsx | 176 +++++ .../MyBillboardStatusActions.tsx | 8 +- .../settings/offers/ConfirmModal.tsx | 10 +- src/components/settings/offers/OffersItem.tsx | 25 +- .../settings/profile/AccountActionModals.tsx | 150 +++++ .../settings/profile/ProfileContent.tsx | 51 +- src/components/stories/StoriesBar.tsx | 2 +- src/components/stories/StoryEditor.tsx | 20 +- src/components/stories/StoryViewer.tsx | 215 +++++- src/components/ui/SocialStatButton.tsx | 119 ++++ src/components/ui/alert-dialog.tsx | 4 +- src/components/ui/button.tsx | 29 +- src/components/ui/dialog.tsx | 4 +- src/components/ui/drawer.tsx | 12 +- src/components/ui/sheet.tsx | 12 +- src/config/pageSeo.ts | 8 +- src/constants/index.ts | 5 + src/constants/pageLayout.ts | 6 + src/constants/reelsFeed.ts | 18 + src/contexts/ExploreReelsUiContext.tsx | 40 ++ src/hooks/useAuthSessionRedirect.ts | 28 + src/hooks/useAxios.tsx | 12 +- src/hooks/usePostSocialActions.tsx | 262 ++++++++ src/hooks/usePwaInstall.ts | 81 +++ src/hooks/useReelsSnapScroll.ts | 69 ++ src/lib/api/fetchApiJson.ts | 32 + src/lib/api/upstreamProxy.ts | 16 +- src/lib/appearanceOptions.ts | 58 ++ src/lib/auth/fetchRegisterStatus.ts | 68 ++ src/lib/auth/postLogin.ts | 13 +- src/lib/auth/registrationRoutes.ts | 85 +++ src/lib/auth/session.ts | 68 +- src/lib/buildAcademyListSeo.ts | 97 +++ src/lib/buildCourseDetailSeo.ts | 32 + src/lib/buildProjectDetailSeo.ts | 18 + src/lib/buildProjectsListSeo.ts | 72 ++ src/lib/chat/deleteWindow.ts | 20 + src/lib/chat/socketClient.ts | 7 +- src/lib/coldStartFeed.ts | 28 +- src/lib/expertiseColors.ts | 55 ++ src/lib/explore/buildPostFilters.ts | 2 + src/lib/explore/postMedia.ts | 36 + src/lib/formatCoursePriceLabel.ts | 15 + src/lib/formatLikeDisplay.ts | 9 + src/lib/getCreateContentPath.ts | 4 + src/lib/isModelExpertise.ts | 6 + src/lib/postSlug.ts | 70 ++ src/lib/pwa/install.ts | 39 ++ src/lib/reelsMediaType.ts | 35 + src/lib/reelsSeedPost.ts | 2 + src/lib/ui/buttonStyles.ts | 66 ++ src/lib/userLevel.ts | 3 + src/lib/videoPreload.ts | 84 +++ src/middleware.ts | 32 +- src/types/types.ts | 18 +- src/utils/generatePageMetadata.ts | 4 +- 239 files changed, 10283 insertions(+), 3041 deletions(-) create mode 100644 public/favicon.ico create mode 100644 public/images/icons/192x192.png create mode 100644 public/images/icons/actions-toggle.svg create mode 100644 public/images/icons/apple-touch-icon.png create mode 100644 public/images/icons/bookmark-post.svg create mode 100644 public/images/icons/collaboration-request.svg create mode 100644 public/images/icons/follow-user.svg create mode 100644 public/images/icons/send-post.svg create mode 100644 public/images/icons/star-pack/star.svg create mode 100644 src/api/fetchBookmarkPosts.ts create mode 100644 src/api/fetchProject.ts create mode 100644 src/app/(projects)/academy/profile/[academyId]/AcademyProfileClient.tsx create mode 100644 src/app/explore/[id]/ExploreReelClient.tsx create mode 100644 src/app/icon.png create mode 100644 src/app/posts/[id]/[[...slug]]/page.tsx delete mode 100644 src/app/posts/[id]/page.tsx create mode 100644 src/app/settings/chats/layout.tsx create mode 100644 src/app/settings/edit/layout.tsx create mode 100644 src/app/settings/favorites/page.tsx create mode 100644 src/app/settings/financial/layout.tsx create mode 100644 src/app/settings/my-billboards/layout.tsx create mode 100644 src/app/settings/notifications/layout.tsx create mode 100644 src/app/settings/offers/layout.tsx create mode 100644 src/app/settings/profile/ProfileClient.tsx create mode 100644 src/app/settings/profile/user-settings/layout.tsx create mode 100644 src/app/settings/profile/user-settings/page.tsx create mode 100644 src/app/settings/profile/visitors/layout.tsx create mode 100644 src/app/settings/profile/visitors/page.tsx create mode 100644 src/app/settings/tickets/layout.tsx create mode 100644 src/app/settings/workroom/layout.tsx create mode 100644 src/components/academy/AcademyFilterModal.tsx create mode 100644 src/components/academy/AcademyPackageItem.tsx create mode 100644 src/components/academy/AcademySkeletons.tsx create mode 100644 src/components/auth/AuthSessionSync.tsx create mode 100644 src/components/auth/RegisterRouteGuard.tsx create mode 100644 src/components/chat/ChatGuideModal.tsx create mode 100644 src/components/chat/MediaPreviewModal.tsx create mode 100644 src/components/chat/SharedPostBubble.tsx create mode 100644 src/components/chat/SharedStoryBubble.tsx create mode 100644 src/components/explore/ExploreVideoThumb.tsx create mode 100644 src/components/models/AppearanceColorSelect.tsx create mode 100644 src/components/models/ModelPage/ProfileUserListModal.tsx create mode 100644 src/components/posts/CaptionWithMentions.tsx create mode 100644 src/components/posts/FollowingSuggestionsCarousel.tsx create mode 100644 src/components/posts/PostCommentsContent.tsx create mode 100644 src/components/posts/ReelsImageCarousel.tsx create mode 100644 src/components/posts/ReelsScrollSlot.tsx create mode 100644 src/components/posts/ReelsTabBar.tsx create mode 100644 src/components/posts/SendPostModal.tsx create mode 100644 src/components/pwa/PwaInstallPrompt.tsx create mode 100644 src/components/settings/FavoritesGrid.tsx create mode 100644 src/components/settings/profile/AccountActionModals.tsx create mode 100644 src/components/ui/SocialStatButton.tsx create mode 100644 src/constants/pageLayout.ts create mode 100644 src/constants/reelsFeed.ts create mode 100644 src/contexts/ExploreReelsUiContext.tsx create mode 100644 src/hooks/useAuthSessionRedirect.ts create mode 100644 src/hooks/usePostSocialActions.tsx create mode 100644 src/hooks/usePwaInstall.ts create mode 100644 src/hooks/useReelsSnapScroll.ts create mode 100644 src/lib/api/fetchApiJson.ts create mode 100644 src/lib/appearanceOptions.ts create mode 100644 src/lib/auth/fetchRegisterStatus.ts create mode 100644 src/lib/buildAcademyListSeo.ts create mode 100644 src/lib/buildCourseDetailSeo.ts create mode 100644 src/lib/buildProjectDetailSeo.ts create mode 100644 src/lib/buildProjectsListSeo.ts create mode 100644 src/lib/chat/deleteWindow.ts create mode 100644 src/lib/expertiseColors.ts create mode 100644 src/lib/explore/postMedia.ts create mode 100644 src/lib/formatCoursePriceLabel.ts create mode 100644 src/lib/formatLikeDisplay.ts create mode 100644 src/lib/isModelExpertise.ts create mode 100644 src/lib/postSlug.ts create mode 100644 src/lib/pwa/install.ts create mode 100644 src/lib/reelsMediaType.ts create mode 100644 src/lib/ui/buttonStyles.ts create mode 100644 src/lib/videoPreload.ts diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..0f6b4bb1924355533f95c4ba58a91c5f3c68147f GIT binary patch literal 11537 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 E1IoRQH2?qr literal 0 HcmV?d00001 diff --git a/public/images/icons/192x192.png b/public/images/icons/192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..92ed3bd55500ca91021c2b36614fdb1b1acd07e8 GIT binary patch literal 11045 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 + + + + + diff --git a/public/images/icons/apple-touch-icon.png b/public/images/icons/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..92ed3bd55500ca91021c2b36614fdb1b1acd07e8 GIT binary patch literal 11045 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 + + diff --git a/public/images/icons/collaboration-request.svg b/public/images/icons/collaboration-request.svg new file mode 100644 index 0000000..df9a495 --- /dev/null +++ b/public/images/icons/collaboration-request.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/icons/follow-user.svg b/public/images/icons/follow-user.svg new file mode 100644 index 0000000..9f0509e --- /dev/null +++ b/public/images/icons/follow-user.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/send-post.svg b/public/images/icons/send-post.svg new file mode 100644 index 0000000..fafe077 --- /dev/null +++ b/public/images/icons/send-post.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/images/icons/star-pack/star.svg b/public/images/icons/star-pack/star.svg new file mode 100644 index 0000000..73ecf2e --- /dev/null +++ b/public/images/icons/star-pack/star.svg @@ -0,0 +1,11 @@ + + + + + + diff --git a/public/manifest.json b/public/manifest.json index 6757fa0..29fdf3f 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,21 +1,41 @@ { - "name": "Modstagram", - "short_name": "Modstagram", - "description": "Modstagram App", + "name": "مدستاگرام", + "short_name": "مدستاگرام", + "description": "شبکه تخصصی مدل، عکاس و آرایشگر", + "lang": "fa", + "dir": "rtl", + "scope": "/", + "start_url": "/", + "id": "/", + "display": "standalone", + "orientation": "portrait-primary", "icons": [ + { + "src": "/images/icons/192x192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any" + }, { "src": "/images/icons/256x256.png", "sizes": "256x256", - "type": "image/png" + "type": "image/png", + "purpose": "any" }, { "src": "/images/icons/512x512.png", "sizes": "512x512", - "type": "image/png" + "type": "image/png", + "purpose": "any" + }, + { + "src": "/images/icons/512x512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" } ], "theme_color": "#ffffff", "background_color": "#ffffff", - "display": "standalone", - "start_url": "/" + "categories": ["social", "business"] } diff --git a/public/service-worker.js b/public/service-worker.js index 726b683..55528b9 100644 --- a/public/service-worker.js +++ b/public/service-worker.js @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ -const CACHE_NAME = "pwa-cache-v2"; +const CACHE_NAME = "pwa-cache-v4"; // نصب Service Worker self.addEventListener("install", (event) => { @@ -23,42 +23,39 @@ self.addEventListener("install", (event) => { self.addEventListener("activate", (event) => { console.log("Service Worker activated."); event.waitUntil( - caches.keys().then((keys) => - Promise.all( - keys - .filter((key) => key !== CACHE_NAME) - .map((key) => caches.delete(key)) + caches + .keys() + .then((keys) => + Promise.all( + keys + .filter((key) => key !== CACHE_NAME) + .map((key) => caches.delete(key)) + ) ) - ).then(() => self.clients.claim()) + .then(() => self.clients.claim()) ); }); -// هندل درخواست‌ها - self.addEventListener("fetch", (event) => { - const url = new URL(event.request.url); - - // استراتژی network-first برای درخواست‌های HTML (navigation requests) - if (event.request.mode === "navigate") { - event.respondWith( - fetch(event.request) - .then(async (response) => { - const cache = await caches.open(CACHE_NAME); - await cache.put(event.request, response.clone()); - return response; - }) - .catch(() => caches.match(event.request)) - ); - return; - } - - // آیکون‌ها: network-first تا تغییرات فوری اعمال شوند +// هندل درخواست‌ها +self.addEventListener("fetch", (event) => { + const url = new URL(event.request.url); + + // navigation: همیشه از شبکه — HTML کش نشود تا title/meta همیشه تازه باشد + if (event.request.mode === "navigate") { + event.respondWith(fetch(event.request)); + return; + } + + // آیکون‌ها: network-first if (url.pathname.startsWith("/images/icons/")) { event.respondWith( fetch(event.request) .then((res) => { if (res.ok) { const clone = res.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + caches + .open(CACHE_NAME) + .then((cache) => cache.put(event.request, clone)); } return res; }) @@ -82,7 +79,9 @@ self.addEventListener("activate", (event) => { const networkFetch = fetch(event.request).then((res) => { if (res.ok) { const clone = res.clone(); - caches.open(CACHE_NAME).then((cache) => cache.put(event.request, clone)); + caches + .open(CACHE_NAME) + .then((cache) => cache.put(event.request, clone)); } return res; }); diff --git a/public/sitemap-0.xml b/public/sitemap-0.xml index 4c53f8b..c4b95eb 100644 --- a/public/sitemap-0.xml +++ b/public/sitemap-0.xml @@ -1,12 +1,50 @@ -https://modstagram.com/sitemap.xml2026-07-16T17:09:58.119Zdaily0.7 -https://modstagram.com/robots.txt2026-07-16T17:09:58.120Zdaily0.7 -https://modstagram.com/academy/payment/success2026-07-16T17:09:58.120Zdaily0.7 -https://modstagram.com/academy/payment/failed2026-07-16T17:09:58.120Zdaily0.7 -https://modstagram.com/about-us2026-07-16T17:09:58.120Zmonthly0.8 -https://modstagram.com/explore2026-07-16T17:09:58.120Zdaily0.7 +https://modstagram.com/icon.png2026-07-17T12:03:53.291Zdaily0.7 +https://modstagram.com/robots.txt2026-07-17T12:03:53.292Zdaily0.7 +https://modstagram.com/sitemap.xml2026-07-17T12:03:53.292Zdaily0.7 +https://modstagram.com/academy/payment/failed2026-07-17T12:03:53.292Zdaily0.7 +https://modstagram.com/academy/payment/success2026-07-17T12:03:53.292Zdaily0.7 +https://modstagram.com/about-us2026-07-17T12:03:53.292Zmonthly0.8 +https://modstagram.com/explore2026-07-17T12:03:53.292Zdaily0.7 https://modstagram.comdaily1 https://modstagram.com/projectsdaily0.9 https://modstagram.com/billboardsdaily0.9 +https://modstagram.com/billboards/6a0d848b12cadb57c40ecd20/%D8%B9%DA%A9%D8%A7%D8%B3%DB%8C%20%D9%88%20%D9%85%D8%AF%D9%84%DB%8C%D9%86%DA%AF%20%DA%A9%D8%AA%DB%8C%20%D9%85%D8%B7%D9%87%D8%B1%DB%8Cdaily0.8 +https://modstagram.com/billboards/6a0d6dd112cadb57c40ebba3/%D9%85%D8%AC%D9%85%D9%88%D8%B9%D9%87%20%D9%84%D9%88%DA%A9%D8%B3%E2%80%8C%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%7C%20%D8%A7%D9%84%D9%87%D9%87%20%D8%AC%D8%B9%D9%81%D8%B1%DB%8Cdaily0.8 +https://modstagram.com/billboards/69a1cf1c1cd06873681b4271/%D9%86%D8%AF%D8%A7%20%D8%AF%D9%87%D9%82%D8%A7%D9%86%20%7C%20%D9%85%DB%8C%DA%A9%D8%A7%D9%BE%20%D8%B9%D8%B1%D9%88%D8%B3%20%D8%A7%D8%B5%D9%81%D9%87%D8%A7%D9%86daily0.8 +https://modstagram.com/billboards/69a1c3411cd06873681b39d6/%D9%81%D8%B1%D8%B2%D8%A7%D9%86%D9%87%20%D8%AC%D9%86%D8%AA%DB%8C%D8%8C%D8%B3%D8%A7%D9%84%D9%86%20%D8%B9%D8%B1%D9%88%D8%B3%D8%8C%D8%A7%D9%93%D9%85%D9%88%D8%B2%D8%B4%20%D9%85%DB%8C%DA%A9%D8%A7%D9%BE%20%D8%A7%D8%B5%D9%81%D9%87%D8%A7%D9%86daily0.8 +https://modstagram.com/billboards/69a1bfd91cd06873681b360d/%D8%A2%D9%85%D9%88%D8%B2%D8%B4%DA%AF%D8%A7%D9%87%20%D9%88%20%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%81%D9%88%D9%82%20%D8%AA%D8%AE%D8%B5%D8%B5%DB%8C%20%DA%A9%D9%85%D9%86%D8%AFdaily0.8 +https://modstagram.com/billboards/69a1b6d11cd06873681b26f3/%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%85%D8%AD%D8%B4%D8%B1daily0.8 +https://modstagram.com/billboards/69a1afa01cd06873681b246d/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%B1%D8%A7%D9%85%DB%8C%D9%86%20%D9%81%D8%B1daily0.8 +https://modstagram.com/billboards/69a0206a83d4cf28444356d6/%D8%A2%D9%85%D9%88%D8%B2%D8%B4%DA%AF%D8%A7%D9%87%20%DB%8C%D8%B9%D9%82%D9%88%D8%A8%20%D9%84%D9%88%20(%D8%AD%DB%8C%D8%B1%D8%A7)daily0.8 +https://modstagram.com/billboards/69a0078d83d4cf2844434af8/%D9%81%D8%A7%D8%B7%D9%85%D9%87%20%D9%85%D8%B1%D9%88%D8%AA%DB%8C%7C%D9%85%DB%8C%DA%A9%D8%A7%D9%BE%20%D8%A7%D8%B1%D8%AF%D8%A8%DB%8C%D9%84%7C%D9%85%DB%8C%DA%A9%D8%A7%D9%BE%20%D8%B9%D8%B1%D9%88%D8%B3%7C%D8%A2%D9%85%D9%88%D8%B2%D8%B4%7C%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%7Cdaily0.8 +https://modstagram.com/billboards/69a0053783d4cf2844434615/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%A7%D8%B1%D8%AF%D8%A8%DB%8C%D9%84%20%7C%D9%85%D8%AF%DB%8C%D8%B3%D8%A7%20%D8%A7%D8%B5%D9%88%D9%84%DB%8Cdaily0.8 +https://modstagram.com/billboards/699ffbef83d4cf2844433523/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%B2%D9%87%D8%B1%D8%A7%20%D9%86%D9%88%DB%8C%D8%AFdaily0.8 +https://modstagram.com/billboards/699ff8d383d4cf28444330f2/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%B4%D8%A7%D9%86%D9%84daily0.8 +https://modstagram.com/billboards/699ff68b83d4cf2844432964/%F0%9F%91%91%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%AA%D9%88%D8%AA%20%D9%81%D8%B1%D9%86%DA%AF%DB%8C%20%D8%A7%D8%B1%D8%AF%D8%A8%DB%8C%D9%84%F0%9F%91%91daily0.8 +https://modstagram.com/billboards/699ff2eb83d4cf2844432403/%D8%B9%D8%B1%D9%88%D8%B3%20%D8%B3%D8%B1%D8%A7%DB%8C%20%D8%A7%D9%84%20%D8%A2%DB%8C%20%7C%20%D8%A7%D8%B9%D8%B8%D9%85%20%D8%AC%D9%84%DB%8C%D9%84%20%D8%B2%D8%A7%D8%AF%D9%87%20%7C%20%D8%A7%D8%B1%D8%AF%D8%A8%DB%8C%D9%84daily0.8 +https://modstagram.com/billboards/699eca0c9ed07500ab2e0e17/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%BE%D8%AF%DB%8C%D8%AF%D9%87%20%D8%B4%D9%87%D8%B1(%D9%85%DB%8C%D9%86%D8%A7%20%D9%85%D9%87%D8%AC%D9%88%D8%B1)daily0.8 +https://modstagram.com/billboards/699ec6b59ed07500ab2e0856/%F0%9F%92%87%F0%9F%8F%BC%E2%80%8D%E2%99%80%EF%B8%8F%D9%85%D8%AC%D9%85%D9%88%D8%B9%D9%87%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%AA%D8%A7%D8%AC%F0%9F%91%91daily0.8 +https://modstagram.com/billboards/699ebafb9ed07500ab2e01ab/%D9%85%D8%AC%D9%85%D9%88%D8%B9%D9%87%20%D8%B3%DB%8C%D9%85%D8%A7%20%D8%AD%D9%85%D8%AF%D8%A7%D9%84%D9%84%D9%87%DB%8C%7C%D8%AA%D8%A8%D8%B1%DB%8C%D8%B2daily0.8 +https://modstagram.com/billboards/699eaf959ed07500ab2df952/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%DA%A9%D8%A7%D8%B1%DB%8C%D8%B2%D9%85%D8%A7daily0.8 +https://modstagram.com/billboards/699eaa049ed07500ab2df3a4/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%85%D9%88%D8%AA%D8%A7%D8%A8daily0.8 +https://modstagram.com/billboards/69942dd60711bf017b2069e2/%F0%9F%AA%84%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%B4%D9%87%D8%B1%D8%B2%D8%A7%D8%AF%F0%9F%AA%84%7C%20shahrzaad%20beautydaily0.8 +https://modstagram.com/billboards/694fad80d93828c638fdf612/Hamid%20Eskandari%20%D9%81%DB%8C%D9%84%D9%85%D8%A8%D8%B1%D8%AF%D8%A7%D8%B1%DB%8C%20%D9%88%20%D8%B9%DA%A9%D8%A7%D8%B3%DB%8Cdaily0.8 +https://modstagram.com/billboards/693447f0a468f7683cca4e55/%E2%9A%9C%EF%B8%8F%20%D8%A8%D8%A7%D9%86%D9%88%20%D8%B1%D8%B6%D8%A7%DB%8C%DB%8C%20%7C%20vip%20salon%20%E2%9A%9C%EF%B8%8Fdaily0.8 +https://modstagram.com/billboards/6934170662e821773ad7a8f3/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%85%D8%B1%D8%AC%D8%A7%D9%86%F0%9F%91%B8daily0.8 +https://modstagram.com/billboards/69340f3e62e821773ad7a555/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%88%20%D9%85%D8%B1%DA%A9%D8%B2%20%D8%A7%DA%A9%D8%B3%D8%AA%D9%86%D8%B4%D9%86%20%DA%AF%D9%84daily0.8 +https://modstagram.com/billboards/691c3ff8e24a347e31d006a6/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20vip%20%D8%B1%DB%8C%D8%AD%D8%A7%D9%86daily0.8 +https://modstagram.com/billboards/691c3a88e24a347e31d001ea/%D8%B9%D9%85%D8%A7%D8%B1%D8%AA%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%DA%86%D9%8E%D9%85%20%7C%20Chambeautysalondaily0.8 +https://modstagram.com/billboards/691c33eee24a347e31cffc27/%D9%81%D8%B1%D9%86%D8%A7%D8%B2%20%D8%B5%D9%81%D8%B1%DB%8C%20%7C%D9%85%DB%8C%DA%A9%D8%A7%D9%BE%20%D8%A7%D8%B1%D8%AA%DB%8C%D8%B3%D8%AA%20%D8%B9%D8%B1%D9%88%D8%B3%7C%D9%85%D8%AF%D8%B1%D8%B3%20%D8%AA%D8%AE%D8%B5%D8%B5%DB%8Cdaily0.8 +https://modstagram.com/billboards/691af3c9e24a347e31cfedef/%D8%B3%D8%A7%D9%84%D9%86%20%D9%85%D9%85%D8%AA%D8%A7%D8%B2%20%D8%B4%D8%B1%D9%82%20%D8%AA%D9%87%D8%B1%D8%A7%D9%86%20%C2%AB%DA%AF%D9%84%20%DA%AF%DB%8C%D8%B3%C2%BBdaily0.8 +https://modstagram.com/billboards/691ae483e24a347e31cfe822/%D8%BA%D8%B2%D9%84%20%D8%B2%D8%B1%DA%AF%D8%B1%DB%8C%D8%A7%D9%86%20%7C%20%D9%85%DB%8C%DA%A9%D8%B1%D9%88%D8%A8%D9%84%DB%8C%D8%AF%DB%8C%D9%86%DA%AF%20%7C%20%D9%81%DB%8C%D8%A8%D8%B1%D9%88%D8%B2daily0.8 +https://modstagram.com/billboards/6918a635e24a347e31cf8b17/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%B3%D9%BE%DB%8C%D8%AF%D9%87%20%D9%85%D9%88%D8%B3%D9%88%DB%8Cdaily0.8 +https://modstagram.com/billboards/6918a247e24a347e31cf883f/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%DA%AF%D9%84%D8%B3%D8%A7%D9%86%20%F0%9F%AA%B7daily0.8 +https://modstagram.com/billboards/691891a8e24a347e31cf7feb/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%DA%AF%D9%84%20%D8%B3%D8%B1%D8%AEdaily0.8 +https://modstagram.com/billboards/69188517e24a347e31cf7241/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%BE%D8%B1%D9%86%D8%B3%D8%B3daily0.8 +https://modstagram.com/billboards/691880ffe24a347e31cf6e55/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%D9%8A%D8%A8%D8%A7%DB%8C%DB%8C%20%D9%86%D9%82%D8%B1%D9%87%20%D9%86%DA%AF%D8%A7%D8%B1daily0.8 +https://modstagram.com/billboards/69187bfbe24a347e31cf6b00/%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%A2%D9%86%D8%AC%D9%84daily0.8 +https://modstagram.com/billboards/691332c9e24a347e31cef09f/%D8%AE%D8%AF%D9%85%D8%A7%D8%AA%20%D8%B3%D8%A7%D9%84%D9%86%20%D8%B2%DB%8C%D8%A8%D8%A7%DB%8C%DB%8C%20%D8%AF%DA%A9%D8%AA%D8%B1%20%D8%B2%D8%A7%D8%B1%D8%A7daily0.8 +https://modstagram.com/billboards/6a0ef05d12cadb57c40ee4b2/%D8%B9%DA%A9%D8%A7%D8%B3%DB%8C%20%D8%A8%D8%A8%D8%B1%DB%8C%20%D8%A2%D8%B1%D8%AAdaily0.8 \ No newline at end of file diff --git a/src/api/fetchBookmarkPosts.ts b/src/api/fetchBookmarkPosts.ts new file mode 100644 index 0000000..3714041 --- /dev/null +++ b/src/api/fetchBookmarkPosts.ts @@ -0,0 +1,32 @@ +import { getApiBaseUrl } from "@/components/main/BaseUrl"; +import { Post } from "@/types/types"; + +export async function fetchBookmarkPosts( + page: number, + limit: number, + token: string +): Promise<{ posts: Post[]; hasMore: boolean; totalItems: number }> { + const base = getApiBaseUrl(); + const res = await fetch( + `${base}/posts/bookmarks?page=${page}&limit=${limit}`, + { + cache: "no-store", + headers: { Authorization: `Bearer ${token}` }, + } + ); + + if (!res.ok) { + return { posts: [], hasMore: false, totalItems: 0 }; + } + + const data = await res.json(); + const posts = (data?.posts ?? []) as Post[]; + const totalItems = data?.totalItems ?? posts.length; + const totalPages = data?.totalPages ?? 1; + + return { + posts, + hasMore: page < totalPages, + totalItems, + }; +} diff --git a/src/api/fetchPosts.ts b/src/api/fetchPosts.ts index 3598abb..0e931d4 100644 --- a/src/api/fetchPosts.ts +++ b/src/api/fetchPosts.ts @@ -11,7 +11,7 @@ export async function fetchPosts( userLevel?: string; rateFilter?: string; _id?: string; - type?: string; + type?: "image" | "video"; exploreFilter?: string; subExpertise?: string; lat?: string; @@ -19,6 +19,17 @@ export async function fetchPosts( feedMode?: "grid" | "reels"; seedPostId?: string; sort?: "latest"; + reelsTab?: "for_you" | "following" | "saved"; + heightMin?: string; + heightMax?: string; + weightMin?: string; + weightMax?: string; + sizeMin?: string; + sizeMax?: string; + hair_color?: string; + eye_color?: string; + q?: string; + hashtag?: string; }, token: string ) { @@ -53,5 +64,16 @@ export async function fetchPosts( throw new Error(`Network response was not ok: ${response.status}`); } - return response.json(); + return response.json() as Promise<{ + posts: unknown[]; + totalPages?: number; + totalItems?: number; + feedMeta?: { + isColdStart?: boolean; + emptyFollowing?: boolean; + emptySaved?: boolean; + requiresAuth?: boolean; + suggestedUsers?: unknown[]; + }; + }>; } \ No newline at end of file diff --git a/src/api/fetchProject.ts b/src/api/fetchProject.ts new file mode 100644 index 0000000..09dd59e --- /dev/null +++ b/src/api/fetchProject.ts @@ -0,0 +1,23 @@ +import { getApiBaseUrl } from "@/components/main/BaseUrl"; +import { Project } from "@/types/types"; + +export async function fetchProject( + id: string, + token = "" +): Promise { + try { + const response = await fetch(`${getApiBaseUrl()}/projects/get/web/${id}`, { + cache: "no-store", + headers: { + Authorization: token ? `Bearer ${token}` : "", + }, + }); + + if (!response.ok) return null; + + const data = await response.json(); + return data?.project ?? null; + } catch { + return null; + } +} diff --git a/src/api/fetchStories.ts b/src/api/fetchStories.ts index a676391..24e3b6a 100644 --- a/src/api/fetchStories.ts +++ b/src/api/fetchStories.ts @@ -19,6 +19,8 @@ export type StoryItem = { createdAt?: string; expires_at?: string; viewed?: boolean; + liked?: boolean; + likes_count?: number; }; export type StoryFeedUser = { @@ -103,3 +105,64 @@ export async function updateStoryOverlays( throw new Error(json?.message || "خطا در ویرایش استوری"); } } + +export type StoryViewerUser = { + _id: string; + user_name: string; + first_name: string; + last_name: string; + profile_image?: string; + viewed_at?: string; +}; + +export async function likeStory( + storyId: string, + token: string +): Promise<{ liked: boolean; already?: boolean }> { + const res = await fetch(`${getApiBaseUrl()}/stories/like`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ storyId }), + }); + if (!res.ok) { + const json = await res.json().catch(() => ({})); + throw new Error(json?.message || "خطا در لایک استوری"); + } + return res.json(); +} + +export async function commentOnStory( + storyId: string, + comment: string, + token: string +): Promise { + const res = await fetch(`${getApiBaseUrl()}/stories/comment`, { + method: "POST", + headers: { + "Content-Type": "application/json", + Authorization: `Bearer ${token}`, + }, + body: JSON.stringify({ storyId, comment }), + }); + if (!res.ok) { + const json = await res.json().catch(() => ({})); + throw new Error(json?.message || "خطا در ارسال کامنت"); + } +} + +export async function fetchStoryViewers( + storyId: string, + token: string +): Promise<{ viewers: StoryViewerUser[]; total: number }> { + const res = await fetch(`${getApiBaseUrl()}/stories/${storyId}/viewers`, { + headers: { Authorization: `Bearer ${token}` }, + }); + if (!res.ok) { + const json = await res.json().catch(() => ({})); + throw new Error(json?.message || "خطا در دریافت بازدیدکنندگان"); + } + return res.json(); +} diff --git a/src/app/(auth)/(login)/login-with-username/page.tsx b/src/app/(auth)/(login)/login-with-username/page.tsx index c02f0e2..cbe2722 100644 --- a/src/app/(auth)/(login)/login-with-username/page.tsx +++ b/src/app/(auth)/(login)/login-with-username/page.tsx @@ -8,7 +8,7 @@ import AuthPasswordInput from "@/components/auth/AuthPasswordInput"; import Container from "@/components/elements/Container"; import AuthRules from "@/components/auth/AuthRules"; import Link from "next/link"; -import React, { useEffect, useState } from "react"; +import React, { useState } from "react"; import useAxios from "@/hooks/useAxios"; import { useFormik } from "formik"; import * as yup from "yup"; @@ -19,8 +19,8 @@ import { getAxiosErrorMessage, getSafeRedirectPath, } from "@/lib/auth/postLogin"; -import { clearAuthSession } from "@/lib/auth/session"; import toast from "react-hot-toast"; +import { useAuthSessionRedirect } from "@/hooks/useAuthSessionRedirect"; const schema = yup.object().shape({ username: yup.string().required("نام کاربری الزامی است"), @@ -38,10 +38,7 @@ function LoginWithUsername() { const [errorMessage, setErrorMessage] = useState(""); const { request, loading } = useAxios(); const redirectPath = getSafeRedirectPath(); - - useEffect(() => { - void clearAuthSession(); - }, []); + useAuthSessionRedirect(); const formik = useFormik({ initialValues: { diff --git a/src/app/(auth)/(login)/login/page.tsx b/src/app/(auth)/(login)/login/page.tsx index 499469f..9fb93ba 100644 --- a/src/app/(auth)/(login)/login/page.tsx +++ b/src/app/(auth)/(login)/login/page.tsx @@ -24,11 +24,13 @@ const schema = yup.object({ import { getSafeRedirectPath } from "@/lib/auth/postLogin"; import GoogleSignInButton from "@/components/auth/GoogleSignInButton"; import { MOBILE_NUMERIC_INPUT_PROPS } from "@/lib/auth/inputProps"; +import { useAuthSessionRedirect } from "@/hooks/useAuthSessionRedirect"; function Login() { const router = useRouter(); const [isModalOpen, setModalOpen] = useState(false); const { request, loading } = useAxios(); + useAuthSessionRedirect(); const redirectPath = getSafeRedirectPath(); const usernameLoginHref = redirectPath !== "/" diff --git a/src/app/(auth)/(login)/verify-otp/page.tsx b/src/app/(auth)/(login)/verify-otp/page.tsx index e33e103..306c3f4 100644 --- a/src/app/(auth)/(login)/verify-otp/page.tsx +++ b/src/app/(auth)/(login)/verify-otp/page.tsx @@ -14,9 +14,10 @@ import { useFormik } from "formik"; import * as yup from "yup"; import { useRouter } from "next/navigation"; import { IVerifyOtp } from "@/types/types"; -import { setAuthSession } from "@/lib/auth/session"; -import { AUTH_SUCCESS_DELAY_MS, pause } from "@/lib/auth/feedback"; -import { getRegistrationRoute } from "@/lib/auth/registrationRoutes"; +import { + completeLogin, + getSafeRedirectPath, +} from "@/lib/auth/postLogin"; // Validation schema using Yup const schema = yup.object({ @@ -57,26 +58,14 @@ function VerifyOtp() { switch (response?.page) { case "home": - await setAuthSession(response.token, { - id: response.id, - user_type: response.user_type, - step: response.step, - auth_provider: response.auth_provider, - email: response.email ?? undefined, - }); setIsSuccess(true); - await pause(AUTH_SUCCESS_DELAY_MS); - router.refresh(); - router.push("/"); + await completeLogin(router, response, { + redirectTo: getSafeRedirectPath(), + }); break; case "auth-page": - await setAuthSession(response.token, { - id: response.id, - step: response.step, - }); setIsSuccess(true); - await pause(AUTH_SUCCESS_DELAY_MS); - router.push(getRegistrationRoute(response.step)); + await completeLogin(router, response); break; default: break; diff --git a/src/app/(auth)/(register)/register-otp/page.tsx b/src/app/(auth)/(register)/register-otp/page.tsx index 3be873e..cdc1546 100644 --- a/src/app/(auth)/(register)/register-otp/page.tsx +++ b/src/app/(auth)/(register)/register-otp/page.tsx @@ -15,9 +15,7 @@ import { useFormik } from "formik"; import * as yup from "yup"; import { IVerifyOtp } from "@/types/types"; import { useRouter } from "next/navigation"; -import { setAuthSession } from "@/lib/auth/session"; -import { AUTH_SUCCESS_DELAY_MS, pause } from "@/lib/auth/feedback"; -import { getRegistrationRoute } from "@/lib/auth/registrationRoutes"; +import { completeLogin } from "@/lib/auth/postLogin"; // Validation schema using Yup const schema = yup.object({ @@ -56,22 +54,8 @@ function RegisterOtp() { otp: values.otp.trim(), })) as IVerifyOtp; await localStorage.setItem("otp", values.otp); - await setAuthSession(response.token, { - id: response.id, - step: response.step, - user_type: response.user_type, - auth_provider: response.auth_provider, - email: response.email ?? undefined, - }); setIsSuccess(true); - await pause(AUTH_SUCCESS_DELAY_MS); - - if (response.page === "home") { - router.push("/"); - return; - } - - router.push(getRegistrationRoute(response.step)); + await completeLogin(router, response); } catch (err: any) { setOtpError(true); setIsSuccess(false); diff --git a/src/app/(auth)/(register)/register/complete/page.tsx b/src/app/(auth)/(register)/register/complete/page.tsx index cd9cfe1..f91fcf5 100644 --- a/src/app/(auth)/(register)/register/complete/page.tsx +++ b/src/app/(auth)/(register)/register/complete/page.tsx @@ -54,7 +54,7 @@ function RegisterCompletePage() { finishRegistration("continue")} - className="!border-[#0C8002] !text-[#0C8002] w-full sm:w-auto" + className=" 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 1cdd88d..5788cab 100644 --- a/src/app/(auth)/(register)/register/fullname/page.tsx +++ b/src/app/(auth)/(register)/register/fullname/page.tsx @@ -152,7 +152,7 @@ function FullNamePage() { finishRegistration("continue")} - className="!border-[#0C8002] !text-[#0C8002] w-full sm:w-auto" + className=" w-full sm:w-auto" disabled={Boolean(choiceLoading)} loading={choiceLoading === "continue"} > diff --git a/src/app/(auth)/AuthProviders.tsx b/src/app/(auth)/AuthProviders.tsx index 5bacdcb..7ff1ce2 100644 --- a/src/app/(auth)/AuthProviders.tsx +++ b/src/app/(auth)/AuthProviders.tsx @@ -1,11 +1,16 @@ "use client"; import { GoogleAuthRootProvider } from "@/components/auth/GoogleSignInButton"; +import RegisterRouteGuard from "@/components/auth/RegisterRouteGuard"; export default function AuthProviders({ children, }: { children: React.ReactNode; }) { - return {children}; + return ( + + {children} + + ); } diff --git a/src/app/(auth)/layout.tsx b/src/app/(auth)/layout.tsx index 2bf4abb..d9a1192 100644 --- a/src/app/(auth)/layout.tsx +++ b/src/app/(auth)/layout.tsx @@ -4,7 +4,6 @@ import AuthProviders from "./AuthProviders"; export const metadata: Metadata = { title: { default: "ورود و ثبت‌نام | مدستاگرام", - template: "%s | مدستاگرام", }, description: "ورود یا ثبت‌نام در مدستاگرام", robots: { index: false, follow: false }, diff --git a/src/app/(auth)/verify/colors/page.tsx b/src/app/(auth)/verify/colors/page.tsx index 0a4336e..f71d675 100644 --- a/src/app/(auth)/verify/colors/page.tsx +++ b/src/app/(auth)/verify/colors/page.tsx @@ -5,7 +5,7 @@ import AuthPageLayout, { AuthFormFooter, AuthPageContent, } from "@/components/auth/AuthPageLayout"; -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import useAxios from "@/hooks/useAxios"; import * as yup from "yup"; import { useRouter } from "next/navigation"; @@ -28,14 +28,16 @@ function Colors() { const { request, loading } = useAxios(); const mobile = typeof window !== "undefined" ? localStorage.getItem("mobile") : null; - const [expertise] = useState( - typeof window !== "undefined" ? localStorage.getItem("expertise") || "" : "" - ); - - if (expertise !== "مدل") { - router.push("/verify/public-relations"); + const [expertise] = useState( + typeof window !== "undefined" ? localStorage.getItem("expertise") || "" : "" + ); + + useEffect(() => { + if (expertise && expertise !== "مدل") { + router.replace("/verify/public-relations"); } - + }, [expertise, router]); + const handleCheck = async () => { try { await schema.validate({ selectedHairImage, selectedEyeImage }); @@ -105,7 +107,7 @@ function Colors() { )} @@ -754,7 +834,9 @@ useEffect(() => { {/* قیمت */}
- {courses[0]?.offerNumber > 0 ? ( + {courses[0]?.is_free ? ( + رایگان + ) : courses[0]?.offerNumber > 0 ? (
@@ -786,12 +868,29 @@ useEffect(() => {
) : ( + )} + + {hasContactInfo && courseContactInfo && ( + )} @@ -905,6 +1004,20 @@ useEffect(() => {
+ + {hasContactInfo && courseContactInfo && ( + + )} ); } \ 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 e923828..c32ee3f 100644 --- a/src/app/(projects)/academy/[id]/[title]/page.tsx +++ b/src/app/(projects)/academy/[id]/[title]/page.tsx @@ -1,6 +1,8 @@ import { Metadata } from "next"; import CourseDetail from "./CourseClient"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; +import { getApiBaseUrl } from "@/components/main/BaseUrl"; +import { buildCourseDetailSeo } from "@/lib/buildCourseDetailSeo"; type Props = { params: Promise<{ id: string; title: string }>; @@ -11,7 +13,7 @@ export async function generateMetadata({ params }: Props): Promise { try { const res = await fetch( - `https://app.modstagram.ir/api/v1/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}`, + `${getApiBaseUrl()}/academy/academy/get/curses?page=1&limit=20&status=accept&_id=${id}`, { cache: "no-store" } ); @@ -22,19 +24,12 @@ export async function generateMetadata({ params }: Props): Promise { response?.[0]; if (course) { - const courseName = course.cuorse_name || "دوره آموزشی"; - const category = course.category || "دسته‌بندی نامشخص"; - const teacher = course.teacher_name || "مدرس نامشخص"; - - const rawCaption = course.caption || ""; - const cleanCaption = rawCaption - .replace(/<[^>]*>?/gm, "") - .substring(0, 150); + const seo = buildCourseDetailSeo(course, id); return generatePageMetadata({ - title: `${courseName} | ${category} | آموزشگاه مدستاگرام`, - description: `${cleanCaption || courseName}. دسته: ${category}. مدرس: ${teacher}. پکیج آموزشی تخصصی در آموزشگاه مدستاگرام.`, - path: `/academy/${id}/${encodeURIComponent(slugTitle || courseName)}`, + title: seo.title, + description: seo.description, + path: seo.path, type: "article", }); } @@ -43,8 +38,8 @@ export async function generateMetadata({ params }: Props): Promise { } return generatePageMetadata({ - title: "جزئیات دوره | آموزشگاه مدستاگرام", - description: "مشاهده جزئیات پکیج آموزشی در آموزشگاه مدستاگرام.", + title: "جزئیات پکیج | مدستاگرام", + description: "مشاهده جزئیات پکیج آموزشی در مدستاگرام.", path: `/academy/${id}/${encodeURIComponent(slugTitle || "course")}`, }); } diff --git a/src/app/(projects)/academy/payment/failed/page.tsx b/src/app/(projects)/academy/payment/failed/page.tsx index 39b4fb7..040da9f 100644 --- a/src/app/(projects)/academy/payment/failed/page.tsx +++ b/src/app/(projects)/academy/payment/failed/page.tsx @@ -107,7 +107,7 @@ function FailedProject() { onClick={() => { payHandler(); }} - className="w-32 h-9 !border-[#0C8002] text-[#0C8002]" + variant="primary" className="w-32 h-9" > پرداخت مجدد diff --git a/src/app/(projects)/academy/payment/success/page.tsx b/src/app/(projects)/academy/payment/success/page.tsx index 8134554..af97e4b 100644 --- a/src/app/(projects)/academy/payment/success/page.tsx +++ b/src/app/(projects)/academy/payment/success/page.tsx @@ -80,7 +80,7 @@ function SuccessProject() { پروژه شما پس از بررسی توسط کارشناسان ما منتشر خواهد شد

- + اتاق کار diff --git a/src/app/(projects)/academy/profile/[academyId]/AcademyProfileClient.tsx b/src/app/(projects)/academy/profile/[academyId]/AcademyProfileClient.tsx new file mode 100644 index 0000000..cdba142 --- /dev/null +++ b/src/app/(projects)/academy/profile/[academyId]/AcademyProfileClient.tsx @@ -0,0 +1,333 @@ +"use client"; + +import ProfileAvatar from "@/components/main/ProfileAvatar"; +import VerificationBadge from "@/components/main/VerificationBadge"; +import AcademyPackageItem from "@/components/academy/AcademyPackageItem"; +import { + AcademyPackageListSkeleton, + AcademyProfileHeadSkeleton, +} from "@/components/academy/AcademySkeletons"; +import Container from "@/components/elements/Container"; +import RoundedButton from "@/components/elements/RoundedButton"; +import { IMAGE_BASE_URL } from "@/components/main/BaseUrl"; +import useAxios from "@/hooks/useAxios"; +import { useUserById } from "@/hooks/getUserById"; +import { Course, IContactInfo } from "@/types/types"; +import Image from "next/image"; +import Link from "next/link"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import toast from "react-hot-toast"; + +interface AcademyStats { + packagesCount: number; + soldCount: number; + totalVideos: number; +} + +interface AcademyOwner { + _id: string; + user_name?: string; + first_name?: string; + last_name?: string; + is_verified?: string; + is_Register?: string | boolean; + user_score?: string | number; +} + +interface AcademyData { + _id: string; + userId?: string; + academy_name?: string; + academy_image?: string; + bio?: string; + tag?: string; + rate?: number; + number_of_rate?: number; + stats?: AcademyStats; + owner?: AcademyOwner; + contactInfo?: IContactInfo; +} + +function openContact( + type: "mobile" | "whatsapp" | "telegram" | "instagram", + contact?: IContactInfo +) { + if (!contact) { + toast.error("اطلاعات تماس ثبت نشده است."); + return; + } + + if (type === "mobile") { + const phone = contact.mobile || contact.phone; + if (!phone) { + toast.error("شماره موبایل ثبت نشده است."); + return; + } + window.open(`tel:${phone}`, "_self"); + return; + } + + if (type === "whatsapp") { + if (!contact.whatsappNumber) { + toast.error("شماره واتساپ ثبت نشده است."); + return; + } + window.open( + `https://wa.me/${contact.whatsappNumber.replace(/\D/g, "")}`, + "_blank" + ); + return; + } + + if (type === "telegram") { + if (!contact.telegramLink) { + toast.error("آیدی تلگرام ثبت نشده است."); + return; + } + window.open(`https://t.me/${contact.telegramLink.replace("@", "")}`, "_blank"); + return; + } + + if (!contact.instagramLink) { + toast.error("آیدی اینستاگرام ثبت نشده است."); + return; + } + window.open( + `https://instagram.com/${contact.instagramLink.replace("@", "")}`, + "_blank" + ); +} + +export default function AcademyProfileClient() { + const params = useParams(); + const academyId = params?.academyId as string; + const { request } = useAxios(); + + const [academy, setAcademy] = useState(null); + const [courses, setCourses] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [isLoadingCourses, setIsLoadingCourses] = useState(true); + + const ownerUserId = academy?.owner?._id || academy?.userId || ""; + const ownerFromApi = useUserById(ownerUserId); + + useEffect(() => { + const fetchAcademy = async () => { + if (!academyId) { + setIsLoading(false); + return; + } + + setIsLoading(true); + try { + const response = await request("POST", "/academy/findAcademyById", { + _id: academyId, + }); + const academyData = + response?.academy || response?.data?.academy || response; + setAcademy(academyData); + } catch { + toast.error("خطا در دریافت اطلاعات آموزشگاه"); + } finally { + setIsLoading(false); + } + }; + + void fetchAcademy(); + }, [academyId, request]); + + useEffect(() => { + const fetchCourses = async () => { + if (!academyId) return; + setIsLoadingCourses(true); + try { + const response = await request( + "GET", + `/academy/academy/get/getAcademyCourse/${academyId}?page=1&limit=50&status=accept` + ); + const list = + response?.data?.courses || response?.courses || []; + setCourses(list); + } catch { + toast.error("خطا در دریافت پکیج‌ها"); + setCourses([]); + } finally { + setIsLoadingCourses(false); + } + }; + + if (academyId) void fetchCourses(); + }, [academyId, request]); + + if (isLoading) { + return ( + + + + + ); + } + + if (!academy) { + return ( + +
آموزشگاه یافت نشد.
+
+ ); + } + + const academyName = academy.academy_name || "آموزشگاه"; + const owner = academy.owner; + const displayUserName = owner?.user_name || ownerFromApi?.user_name; + const displayUserScore = + owner?.user_score ?? ownerFromApi?.user_score ?? 0; + const isVerified = owner?.is_verified ?? ownerFromApi?.is_verified; + const isRegister = owner?.is_Register ?? ownerFromApi?.is_Register; + const stats = academy.stats; + const shareUrl = `modstagram.com/academy/profile/${academyId}`; + + return ( + +
+
{ + navigator.clipboard.writeText(shareUrl); + toast.success("لینک آموزشگاه کپی شد"); + }} + > + copy icon + {shareUrl} +
+ +
+
+
+
+ {stats?.totalVideos ?? 0} + کل ویدئوها +
+
+ {stats?.packagesCount ?? 0} + تعداد پکیج‌ها +
+
+ {stats?.soldCount ?? 0} + تعداد فروخته شده +
+
+
+ +
+ +
+
+
+ {Number(academy.rate || 0).toFixed(1)} + امتیاز +
+
+ {displayUserScore || 0} + امتیاز کاربر +
+
+
+

{academyName}

+ {displayUserName ? ( + + {displayUserName} + + + ) : null} +
+
+ +
+

+ {academy.bio || "توضیحاتی برای این آموزشگاه ثبت نشده است."} +

+
+ +
+
+ + + + openContact("instagram", academy.contactInfo)} + className="text-[9px] md:text-sm h-7 md:h-8 max-sm:text-[8px]" + > + اینستاگرام + +
+
+
+ +
+

پکیج‌های آموزشی

+ {isLoadingCourses ? ( + + ) : courses.length === 0 ? ( +

+ هنوز پکیجی برای این آموزشگاه ثبت نشده است. +

+ ) : ( + courses.map((course) => ( + + )) + )} +
+
+ ); +} diff --git a/src/app/(projects)/academy/profile/[academyId]/page.tsx b/src/app/(projects)/academy/profile/[academyId]/page.tsx index 90de6c8..3460e67 100644 --- a/src/app/(projects)/academy/profile/[academyId]/page.tsx +++ b/src/app/(projects)/academy/profile/[academyId]/page.tsx @@ -1,381 +1,43 @@ -"use client"; +import { Metadata } from "next"; +import AcademyProfileClient from "./AcademyProfileClient"; +import { generatePageMetadata } from "@/utils/generatePageMetadata"; +import { getApiBaseUrl } from "@/components/main/BaseUrl"; -import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl"; -import React, { useEffect, useState } from "react"; -import { Academy, Course } from "@/types/types"; -import Container from "@/components/elements/Container"; -import useAxios from "@/hooks/useAxios"; -import { useParams, useRouter } from "next/navigation"; -import toast from "react-hot-toast"; -import Image from "next/image"; -import Link from "next/link"; -import { motion } from "framer-motion"; +type Props = { + params: Promise<{ academyId: string }>; +}; -interface Tag { - value: string; -} +export async function generateMetadata({ params }: Props): Promise { + const { academyId } = await params; -interface PaginationType { - currentPage: number; - totalPages: number; - totalItems: number; - hasNextPage: boolean; - hasPrevPage: boolean; -} -function AcademyPage() { - const params = useParams(); - const academyId = params?.academyId as string; - - const { request } = useAxios(); - const [academy, setAcademy] = useState(null); - const [courses, setCourses] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [isLoadingCourses, setIsLoadingCourses] = useState(false); - const [pagination, setPagination] = useState({ - currentPage: 1, - totalPages: 1, - totalItems: 0, - hasNextPage: false, - hasPrevPage: false, + try { + const res = await fetch(`${getApiBaseUrl()}/academy/findAcademyById`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ _id: academyId }), + cache: "no-store", + }); + const data = await res.json(); + const academy = data?.academy; + + if (academy?.academy_name) { + return generatePageMetadata({ + title: `${academy.academy_name} | مدستاگرام`, + description: academy.bio || academy.academy_name, + path: `/academy/profile/${academyId}`, + }); + } + } catch { + // fallback below + } + + return generatePageMetadata({ + title: "آموزشگاه | مدستاگرام", + description: "صفحه آموزشگاه در مدستاگرام", + path: `/academy/profile/${academyId}`, }); - const router = useRouter(); - - // دریافت اطلاعات آکادمی - useEffect(() => { - const fetchAcademy = async () => { - if (!academyId) { - console.error("academyId یافت نشد"); - setIsLoading(false); - return; - } - - setIsLoading(true); - - try { - const response = await request("POST", `/academy/findAcademyById`, { - _id: academyId - }); - - let academyData = null; - if (response?.academy) { - academyData = response.academy; - } else if (response?.data?.academy) { - academyData = response.data.academy; - } else { - academyData = response; - } - - setAcademy(academyData); - } catch (error) { - console.error("خطا در دریافت اطلاعات:", error); - toast.error("خطا در دریافت اطلاعات آکادمی"); - } finally { - setIsLoading(false); - } - }; - - fetchAcademy(); - }, [academyId]); - - // دریافت دوره‌های آکادمی - const fetchCourses = async (pageNum: number = 1) => { - if (!academyId) return; - - setIsLoadingCourses(true); - try { - const response = await request( - "GET", - `/academy/academy/get/getAcademyCourse/${academyId}?page=${pageNum}&limit=6&status=accept` - ); - - console.log("دوره‌های آکادمی:", response); - - if (response?.success && response?.data?.courses) { - setCourses(response.data.courses); - if (response.data.pagination) { - setPagination({ - currentPage: response.data.pagination.currentPage, - totalPages: response.data.pagination.totalPages, - totalItems: response.data.pagination.totalItems, - hasNextPage: response.data.pagination.hasNextPage, - hasPrevPage: response.data.pagination.hasPrevPage, - }); - } - } else if (response?.data?.courses) { - setCourses(response.data.courses); - } else { - setCourses([]); - } - } catch (err) { - console.error("خطا در دریافت دوره‌ها:", err); - toast.error("خطا در دریافت دوره‌های آکادمی"); - setCourses([]); - } finally { - setIsLoadingCourses(false); - } - }; - - // بارگذاری اولیه دوره‌ها بعد از دریافت آکادمی - useEffect(() => { - if (academyId) { - fetchCourses(1); - } - }, [academyId]); - - // تغییر صفحه - const handlePageChange = (newPage: number) => { - if (newPage >= 1 && newPage <= pagination.totalPages) { - fetchCourses(newPage); - window.scrollTo({ top: 600, behavior: "smooth" }); - } - }; - - // پردازش تگ‌ها - const parseTags = (tagString: string): Tag[] => { - if (!tagString) return []; - try { - return JSON.parse(tagString); - } catch { - return []; - } - }; - - const tags = parseTags(academy?.tag as string || "[]"); - - if (isLoading) { - return ( - -
-
-
-
-
-

در حال بارگذاری...

-
-
- ); - } - - if (!academy && !isLoading) { - return ( - -
-
🏫
-

- آکادمی یافت نشد -

-

- متأسفیم، آکادمی مورد نظر شما وجود ندارد یا حذف شده است. -

- -
-
- ); - } - - const academyName = academy?.academy_name || "آکادمی"; - const academyImage = academy?.academy_image - ? `${IMAGE_BASE_URL}${academy.academy_image}` - : "/images/default-academy.jpg"; - const academyBio = academy?.bio || "این آکادمی هنوز توضیحاتی اضافه نکرده است."; - const academyRate = academy?.rate || 0; - const numberOfRate = academy?.number_of_rate || 0; - const totalCourses = pagination.totalItems; - const createdAt = academy?.createdAt ? new Date(academy.createdAt).toLocaleDateString("fa-IR") : "نامشخص"; - - - return ( - - {/* هدر حرفه‌ای کامل با تمام اطلاعات */} -
- {/* Background Cover with Gradient */} -
-
-
-
- - {/* Content */} -
-
- {/* لوگو آکادمی - سمت راست در دسکتاپ */} - -
- {academyName} { - (e.target as HTMLImageElement).src = - "/images/default-academy.jpg"; - }} - /> -
-
- - {/* اطلاعات آکادمی - سمت چپ در دسکتاپ */} - -

- {academyName} -

- - {/* توضیحات آکادمی */} -

- {academyBio} -

- - {/* تگ‌ها */} - {tags.length > 0 && ( -
- {tags.map((tag, index) => ( - - #{tag.value} - - ))} -
- )} - - -
-
-
-
- - {/* کارت‌های آمار جزئی */} - -
-
📚
-
- {totalCourses} -
-
دوره آموزشی
-
- -
-
📅
-
- {createdAt || "جدید"} -
-
تاسیس
-
-
- - {/* لیست دوره‌ها */} - -
-

- 📚 دوره‌های آموزشی -

- {totalCourses} دوره -
- - {courses.length === 0 ? ( -
-
📚
-

- هنوز دوره‌ای برای این آکادمی ثبت نشده است. -

-
- ) : ( -
- {courses.map((course, index) => ( - - -
- {course.cuorse_name} -
- {course.offer && parseInt(course.offer) > 0 && ( -
- 🔥 {course.offer}% تخفیف -
- )} -
- -
-

- {course.cuorse_name} -

-

- {course.caption} -

- -
-
- 👨‍🏫 - {course.teacher_name} -
-
- {course.offer && parseInt(course.offer) > 0 ? ( - <> - - {parseInt(course.price).toLocaleString()} - - {( - (parseInt(course.price) * - (100 - parseInt(course.offer))) / - 100 - ).toLocaleString()} - - ) : ( - parseInt(course.price).toLocaleString() - )} - تومان -
-
-
- -
- ))} -
- )} -
-
- ); } -export default AcademyPage; +export default function AcademyProfilePage() { + return ; +} diff --git a/src/app/(projects)/projects/[id]/[title]/page.tsx b/src/app/(projects)/projects/[id]/[title]/page.tsx index 4012359..0642154 100644 --- a/src/app/(projects)/projects/[id]/[title]/page.tsx +++ b/src/app/(projects)/projects/[id]/[title]/page.tsx @@ -1,8 +1,11 @@ import { Metadata } from "next"; import { Suspense } from "react"; +import { cookies } from "next/headers"; import ProjectDetailClient from "./ProjectDetailClient"; import PageLoader from "@/components/ui/PageLoader"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; +import { fetchProject } from "@/api/fetchProject"; +import { buildProjectDetailSeo } from "@/lib/buildProjectDetailSeo"; type ProjectDetailPageProps = { params: Promise<{ id: string; title: string }>; @@ -12,11 +15,23 @@ export async function generateMetadata({ params, }: ProjectDetailPageProps): Promise { const { id, title } = await params; - const decodedTitle = decodeURIComponent(title || "پروژه"); + const token = (await cookies()).get("token")?.value || ""; + const project = await fetchProject(id, token); + if (project) { + const seo = buildProjectDetailSeo(project); + return generatePageMetadata({ + title: seo.title, + description: seo.description, + path: seo.path, + type: "article", + }); + } + + const decodedTitle = decodeURIComponent(title || "پروژه"); return generatePageMetadata({ - title: `${decodedTitle} | پروژه‌های مدستاگرام`, - description: `جزئیات پروژه «${decodedTitle}» در مدستاگرام. مشاهده شرایط همکاری، بودجه و ثبت پیشنهاد.`, + title: `${decodedTitle} - مدستاگرام`, + description: `جزئیات پروژه «${decodedTitle}» در مدستاگرام.`, path: `/projects/${id}/${encodeURIComponent(decodedTitle)}`, }); } diff --git a/src/app/(projects)/projects/page.tsx b/src/app/(projects)/projects/page.tsx index 15bfb74..eb3ba90 100644 --- a/src/app/(projects)/projects/page.tsx +++ b/src/app/(projects)/projects/page.tsx @@ -6,13 +6,10 @@ import { cookies } from "next/headers"; import { Metadata } from "next"; import { pageSeo } from "@/config/pageSeo"; import { generatePageMetadata } from "@/utils/generatePageMetadata"; - -export const metadata: Metadata = generatePageMetadata({ - title: pageSeo.projects.title, - description: pageSeo.projects.description, - path: pageSeo.projects.path, - keywords: [...pageSeo.projects.keywords], -}); +import { + buildProjectsListSeo, + ProjectListFilters, +} from "@/lib/buildProjectsListSeo"; type ProjectsPageProps = { searchParams: Promise<{ @@ -24,27 +21,56 @@ type ProjectsPageProps = { }>; }; -export default async function ProjectsPage({ searchParams }: ProjectsPageProps) { - const filters = await searchParams; - const cookieStore = await cookies(); - const token = cookieStore.get("token")?.value || ""; - - const projectFilters = { +function normalizeProjectFilters( + filters: Awaited +): ProjectListFilters { + return { expertise: filters.expertise || "", most_requests: filters.most_requests || "", most_price: filters.most_price || "", age: filters.age || "", gender: filters.gender || "", }; +} + +export async function generateMetadata({ + searchParams, +}: ProjectsPageProps): Promise { + const filters = normalizeProjectFilters(await searchParams); + const hasFilters = Object.values(filters).some(Boolean); + + if (!hasFilters) { + return generatePageMetadata({ + title: pageSeo.projects.title, + description: pageSeo.projects.description, + path: pageSeo.projects.path, + keywords: [...pageSeo.projects.keywords], + }); + } + + const seo = buildProjectsListSeo(filters); + + return generatePageMetadata({ + title: seo.title, + description: seo.description, + path: seo.path, + keywords: [...pageSeo.projects.keywords], + }); +} + +export default async function ProjectsPage({ searchParams }: ProjectsPageProps) { + const rawFilters = await searchParams; + const projectFilters = normalizeProjectFilters(rawFilters); + const seo = buildProjectsListSeo(projectFilters); + const cookieStore = await cookies(); + const token = cookieStore.get("token")?.value || ""; const initialData = await fetchProjects(1, 10, projectFilters, token); return ( -

- {pageSeo.projects.title.replace(" | مدستاگرام", "")} -

- +

{seo.h1}

+ ; @@ -22,7 +23,15 @@ interface IModelsProps { city?: string; userLevel?: string; rateFilter?: string; - hashtag?: string; // این خط را اضافه کنید + hashtag?: string; + heightMin?: string; + heightMax?: string; + weightMin?: string; + weightMax?: string; + sizeMin?: string; + sizeMax?: string; + hair_color?: string; + eye_color?: string; }>; } @@ -123,11 +132,10 @@ export default async function Models({ params, searchParams }: IModelsProps) { if (urlParams.slug && urlParams.slug.length > 0) { const fullText = decodeURIComponent(urlParams.slug[0]).replace(/-/g, ' '); - // استخراج تخصص (فقط اگر در متن موجود باشد) - if (fullText.includes("مدل")) expertise = "مدل"; - else if (fullText.includes("عکاس")) expertise = "عکاس"; - else if (fullText.includes("آرایشگر")) expertise = "آرایشگر"; - else if (fullText.includes("متخصصین")) expertise = ""; // حالت بدون تخصص + // استخراج تخصص از slug (مدل، عکاس، پوشاک و هر تخصص جدید) + const parsedExpertise = parseExpertiseFromSlug(fullText); + if (parsedExpertise) expertise = parsedExpertise; + else if (fullText.includes("متخصصین")) expertise = ""; const parsedLevel = parseUserLevelFromSlug(fullText); if (parsedLevel) userLevel = parsedLevel; @@ -163,7 +171,7 @@ export default async function Models({ params, searchParams }: IModelsProps) { return ( <>
- + {/* H1 مخفی برای بهبود سئو بر اساس آدرس صفحه */}

{(!urlParams.slug || urlParams.slug.length === 0) && @@ -184,14 +192,21 @@ export default async function Models({ params, searchParams }: IModelsProps) { diff --git a/src/app/billboards/new/[id]/page.tsx b/src/app/billboards/new/[id]/page.tsx index 5d715fc..299a6f6 100644 --- a/src/app/billboards/new/[id]/page.tsx +++ b/src/app/billboards/new/[id]/page.tsx @@ -101,8 +101,9 @@ function BillboardPayment({ params }: IBillboardProps) { مبلغ قابل پرداخت: {Number(price).toLocaleString()} تومان پرداخت diff --git a/src/app/billboards/payment/failed/page.tsx b/src/app/billboards/payment/failed/page.tsx index 368482f..4d40fdb 100644 --- a/src/app/billboards/payment/failed/page.tsx +++ b/src/app/billboards/payment/failed/page.tsx @@ -137,7 +137,7 @@ function FailedBillboard() { payHandler(); } }} - className="w-32 h-9 !border-[#0C8002] text-[#0C8002]" + variant="primary" className="w-32 h-9" > پرداخت مجدد diff --git a/src/app/billboards/payment/success/page.tsx b/src/app/billboards/payment/success/page.tsx index a4968b8..569850b 100644 --- a/src/app/billboards/payment/success/page.tsx +++ b/src/app/billboards/payment/success/page.tsx @@ -81,7 +81,7 @@ function SuccessBillboard() { خواهد شد.

- + بیلورد من diff --git a/src/app/explore/[id]/ExploreReelClient.tsx b/src/app/explore/[id]/ExploreReelClient.tsx new file mode 100644 index 0000000..d52d43a --- /dev/null +++ b/src/app/explore/[id]/ExploreReelClient.tsx @@ -0,0 +1,54 @@ +"use client"; + +import { Suspense } from "react"; +import { useSearchParams } from "next/navigation"; +import ExploreReelsView from "@/components/explore/ExploreReelsView"; +import PostFeedView, { PostFeedSearchParams } from "@/components/posts/PostFeedView"; +import PageLoader from "@/components/ui/PageLoader"; + +function toExploreFeedParams( + searchParams: URLSearchParams +): PostFeedSearchParams { + return { + feed: "explore", + filter: searchParams.get("filter") || undefined, + lat: searchParams.get("lat") || undefined, + lng: searchParams.get("lng") || undefined, + q: searchParams.get("q") || undefined, + hashtag: searchParams.get("hashtag") || undefined, + }; +} + +function ExploreReelContent({ id }: { id: string }) { + const searchParams = useSearchParams(); + const typeParam = searchParams.get("type"); + + if (typeParam === "academy") { + return ( + + ); + } + + return ( + + ); +} + +export default function ExploreReelClient({ id }: { id: string }) { + return ( +
+ }> + + +
+ ); +} diff --git a/src/app/explore/[id]/page.tsx b/src/app/explore/[id]/page.tsx index 92c7aec..c45966d 100644 --- a/src/app/explore/[id]/page.tsx +++ b/src/app/explore/[id]/page.tsx @@ -1,36 +1,42 @@ -"use client"; +import { Metadata } from "next"; +import { fetchPostById } from "@/api/fetchPostById"; +import { buildStorageUrl } from "@/components/main/BaseUrl"; +import { generatePageMetadata } from "@/utils/generatePageMetadata"; +import { + buildPostPath, + buildPostSeoDescription, + buildPostSeoTitle, +} from "@/lib/postSlug"; +import ExploreReelClient from "./ExploreReelClient"; -import { Suspense, use } from "react"; -import { useSearchParams } from "next/navigation"; -import ExploreReelsView from "@/components/explore/ExploreReelsView"; -import PageLoader from "@/components/ui/PageLoader"; - -function ExploreReelContent({ id }: { id: string }) { - const searchParams = useSearchParams(); - const typeParam = searchParams.get("type"); - const initialType = typeParam === "academy" ? "academy" : "post"; - - return ( - - ); -} - -export default function ExploreReelPage({ - params, -}: { +type Props = { params: Promise<{ id: string }>; -}) { - const { id } = use(params); +}; - return ( -
- }> - - -
- ); +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + const post = await fetchPostById(id); + const title = post ? buildPostSeoTitle(post) : "پست | مدستاگرام"; + const description = post + ? buildPostSeoDescription(post) + : "مشاهده پست در اکسپلور مدستاگرام"; + const ogImage = post?.files?.[0]?.path + ? buildStorageUrl(post.files[0].path) + : undefined; + + return generatePageMetadata({ + title, + description, + path: buildPostPath(id, post ?? undefined), + type: "article", + imageUrl: ogImage, + imageAlt: post?.caption?.slice(0, 80) || "پست مدستاگرام", + publishedTime: post?.createdAt, + modifiedTime: post?.updatedAt, + }); +} + +export default async function ExploreReelPage({ params }: Props) { + const { id } = await params; + return ; } diff --git a/src/app/explore/page.tsx b/src/app/explore/page.tsx index 83e3c72..46afa52 100644 --- a/src/app/explore/page.tsx +++ b/src/app/explore/page.tsx @@ -10,12 +10,33 @@ import { pageSeo } from "@/config/pageSeo"; import { useCallback, useState } from "react"; import toast from "react-hot-toast"; +function parseSearchQuery(input: string): { q?: string; hashtag?: string } { + const trimmed = input.trim(); + if (!trimmed) return {}; + if (trimmed.startsWith("#")) { + const tag = trimmed.slice(1).trim(); + return tag ? { hashtag: tag } : {}; + } + return { q: trimmed }; +} + export default function ExplorePage() { const [activeFilter, setActiveFilter] = useState("all"); const [coords, setCoords] = useState<{ lat: number; lng: number } | null>( null ); const [loadingLocation, setLoadingLocation] = useState(false); + const [searchInput, setSearchInput] = useState(""); + const [searchQuery, setSearchQuery] = useState<{ + q?: string; + hashtag?: string; + }>({}); + + const handleSearchSubmit = useCallback((value?: string) => { + const nextValue = value ?? searchInput; + if (value !== undefined) setSearchInput(value); + setSearchQuery(parseSearchQuery(nextValue)); + }, [searchInput]); const handleFilterChange = useCallback((filter: ExploreFilterId) => { if (filter === "near_me") { @@ -62,8 +83,15 @@ export default function ExplorePage() { activeFilter={activeFilter} onFilterChange={handleFilterChange} loadingLocation={loadingLocation} + searchValue={searchInput} + onSearchChange={setSearchInput} + onSearchSubmit={handleSearchSubmit} + /> + - diff --git a/src/app/globals.css b/src/app/globals.css index 2069a53..0ada612 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -404,6 +404,37 @@ select { transition: all 0.35s cubic-bezier(0.25, 0.46, 0.45, 0.94); } +/* ─── Reels / post feed vertical scroll (Instagram-like snap) ─── */ +.reels-vertical-scroll { + scroll-snap-type: y mandatory; + scroll-behavior: auto; + overscroll-behavior-y: contain; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.reels-vertical-scroll > * { + scroll-snap-align: start; + scroll-snap-stop: always; +} +.reels-vertical-scroll::-webkit-scrollbar { + display: none; +} + +.reels-horizontal-scroll { + scroll-snap-type: x mandatory; + scroll-behavior: smooth; + overscroll-behavior-x: contain; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; +} +.reels-horizontal-scroll > * { + scroll-snap-align: start; + scroll-snap-stop: always; +} +.reels-horizontal-scroll::-webkit-scrollbar { + display: none; +} + /* ─── Chat scrollbar ─── */ .custom-scrollbar::-webkit-scrollbar { width: 4px; @@ -656,4 +687,88 @@ select { .profile-avatar { aspect-ratio: 1 / 1; object-fit: cover; +} + +/* ─── Design system: buttons & modals ─── */ +:root { + --btn-border-color: #C3C3C3; + --btn-accent: #FC8EAC; + --btn-accent-hover: #f07898; + --btn-accent-text: #ffffff; +} + +.btn-modern { + border: 1px solid var(--btn-border-color); + transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} +.btn-modern:hover:not(:disabled) { + box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); +} +.btn-modern:active:not(:disabled) { + transform: scale(0.98); +} +.dark .btn-modern { + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.28); +} +.dark .btn-modern:hover:not(:disabled) { + box-shadow: 0 6px 20px rgba(0, 0, 0, 0.38); +} + +.btn-modern--primary { + background: var(--btn-accent); + color: var(--btn-accent-text); + border-color: var(--btn-border-color); +} +.btn-modern--primary:hover:not(:disabled) { + background: var(--btn-accent-hover); +} + +.btn-modern--selected { + background: var(--btn-accent); + color: var(--btn-accent-text); + border-color: var(--btn-accent); +} +.btn-modern--selected:hover:not(:disabled) { + background: var(--btn-accent-hover); + border-color: var(--btn-accent-hover); +} + +.glass-modal-overlay { + background: rgba(8, 8, 10, 0.52); + backdrop-filter: blur(20px) saturate(120%); + -webkit-backdrop-filter: blur(20px) saturate(120%); +} +.dark .glass-modal-overlay { + background: rgba(0, 0, 0, 0.62); + backdrop-filter: blur(22px) saturate(110%); + -webkit-backdrop-filter: blur(22px) saturate(110%); +} +.glass-modal-overlay--dark { + background: rgba(0, 0, 0, 0.72); + backdrop-filter: blur(24px) saturate(110%); + -webkit-backdrop-filter: blur(24px) saturate(110%); +} +.dark .glass-modal-overlay--dark { + background: rgba(0, 0, 0, 0.82); +} + +.glass-modal-panel { + background: rgba(255, 255, 255, 0.78); + backdrop-filter: blur(28px) saturate(180%); + -webkit-backdrop-filter: blur(28px) saturate(180%); + border: 1px solid rgba(255, 255, 255, 0.5); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.14); +} +.dark .glass-modal-panel { + background: rgba(28, 28, 30, 0.84); + border-color: rgba(255, 255, 255, 0.14); + box-shadow: 0 12px 40px rgba(0, 0, 0, 0.48); +} + +.glass-modal-panel--sheet { + border-radius: 1.5rem 1.5rem 0 0; +} +.glass-modal-panel--center { + border-radius: 1.5rem; } \ No newline at end of file diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6b4bb1924355533f95c4ba58a91c5f3c68147f GIT binary patch literal 11537 zcmc(Ebx<6^_va=dKuCg12*KS6?gV!T?jbk?2$ChZ1$Sq0-Nh}qy99?NxCLh+K!C*- znd4hmzx(6v>Z-2ls;;Yg`ZKS)KUMRld*;1QM|@P1e~tAP3j_kaR#cGD1c9C)|5Gua zJzDNCIh!Aw7tRU>-$0<31plc|Kp9zYKp-r2J89{UA8nn$PTy>uoT(J0rKy}EE?Ib zyzpnSu>sLU8f>p3u@*44sS9F*3d18uZa2IO?dRIBcE=wY=Ee6a&a>+#F}g8dr7G}f z@CCdom7>7=6ExI6xUtRgArOns8H9^jXG#6-p7sgo+D}xJm8Kh`6ZFJm<|QVmTQ#$r z>s=Utb0C#&{3IawNjEq_P#H6T5cI(-R;mc}LGDRFb{c~gC=U(vdDOyu3-pm4^qJ9r zcLo%YeVy+81Z0#PE_f^tnKU*2?`tpapgim!h7v0;r}5 z)IEXwvKEB-3dE%m9>NSl^9FtXMN8`e3Qh+R%AM(np0ZZq?y)?2D!o>;g-%E|&H`OFZ0~4qO5CiJ~V*x5KDUJ z&j0a^Y<_&p;^NNs_JUHsl#$tpp6`QAuW7g5y~B;4=-t)DpUzFXKyH&jd9>?4y<iNxt`7FRTXk*eHIFY!Q&!YzgSSu#F zfIy#Q=s2`T>Li9RK_HpDK$aRwGGxyiwr=!yJJZp=%u$cVmj)os7+x>zc&0UJhj6zaDUqxSS zL7{}|9UU*;(hGqX&w@328EKMAlA^=4^81keXGr%~uS1D~hQe)_NHgDHys;s5 ze^Z^NQbalvMLML-CGh?*Z7&@s-APBErIE#lC}9LEMJa_Lg^Yz$yR@vRY_062&biL0 zc3kNvE#E3R9UiT%(igu|%XP}KKOSn4Y1x#Dl!TSplsIWiK>ZBvBhU@{RQlAVp&EHV zO}E+ysa{%;)_Jf|+rKUkbqnO{XZSh3=<}VWE$yDXElRAcG+#)XM~O$TPV+q3S^{G$ z&E+}^toCA$sTgwZ7r91w!o0~$R>Z5)^4`kWzAP1zs$Z^9u2C+3*uh_QRGeMZA=_qf z8;C7Aq{L{p7^|3ARZRGzilfXPMwZ_u3NI`rjN6K_NZ_*|Ss$vKCXPc@`X`aVd*%u>uC8^H-J z)1rm5axro^(RPEzTXb8FgN}oR4CoBh49cnps>d0csx3v#MItI9DnsLA;|1e$>3o@D z@8`MqGWRm)GuN9w8+sVBHI_6sTPfCV8x|QHH@1HbF%&X@>FXGp)X7z5SJKb5Rkju5 z6}PA*sg>u<+4R+%HV!wIHj&osETAt}H99u>n%S6>*fb4P^&Ka5B>A@b#@vfxybfE$ zE+;o6_xo1wN+_%=yp}PMbu8gKfJ{Yj+UQT>PA>3lH4=<6_uFM_WzNXkkl3_*3px*v zictPB$^jAQ$RrzF{nfFp1io-F+346|Lcs6M|}j3HqmdZ7}r z&f)rRr{6>P+^1KhzpcxlWKw0;GHu?g3sti#@v(@PaeNV75@_XZl^Eo(HW-`L&(>FT zP<|)G!BWNl)uD6!?&v&hGkf#pmN!Wn=Pwgy&-{sAWJob_DF;5AaOww@a5cf?f0DQ? zCB)J6X9Vu_u~?qHr33t2?N=%4%kt(BBO=`3`u-Uc#K3ZWFQu{FQHL8h|A_ZgEzAnDv zxdA&?qg2tpwjt9z3r4Mtm7aCic*KZMYIzRp_|aJ1$a1_u)=4gKgx`z5k=O_eRn}uC z*Wc}^_##v*>f*VKwMF!tHB=Y9LbN%uk;QRVz5GZIQsvjd;H7FG_^D^)M^D2O`@@$y zSUVxUhb!|(ChGc}2I~!GsL*UOaiWOGD#B$F2$BB+(~8FqS(wvESNckFeQ9}ri{+#> z+#0dF#-3w@omvLjgcdDBV)nCEDC>RXjW~E+U}~ z)kbQgwE2(Ws|{xJ*I%cP$-LVydH>EX8?|5Eiw}1$>pam>owc5|x>dZbyOOAiC_`MQ z-a498syVtG4LyTDyT_o#Zk%uVJxdN3)7FfeV?C6|9-{1g$Ubprf6f0|Cnjrflh#?r za~pB>V*QHZweZsSXzz**j2X)Bm#HZ&D*1YcImnm_w zcts67mCm(??UoQfW|C~qCZn@cvVHwHZdN`o7c_eBSxl^d!Erii#RYG`Wr3(Yk#>;@ z1NPFDQC zbi3!BPp`;dp@#%bJ|tYk;YdG^OOG23eI0t3BPcE;v?<|wkGLzAp0u1~$RT+sco?lB zA?EbEo;{qOm`N>F9C;000agElYoZ@pyoI5y&e!w2DbhIPu+l`31ILK z5Z?>D4FW97fv+_{#xEeLAJ{zu`qu!?Sm62==vo1?h5?!|Kqdo78v<-9fHyyY*FM1J z2|(x%Kqr7t#ef6^Xq*QK0|3@2fIAL&?hd>O1a?mW93KGP9l-VhaC`yiIDqC3pnV7K z?g5!}VEGUj-2&bP0WUlOJt&}%31E2wb#uVQ4Ujtue5(T}LxAcTAgCQEnFQ2xf$U*G zJ`?zu2Rs&B-^WWpKtNz%;Ex|af`WpAgM&jtLPA4B!@|PC!^0yYA|fLrqoSgsqoZSD zVq#-slao_YQc_b>ArMGfT3UK~dPYV@W@ctqR#tX)c1})C zZfeI>6_u5hRaI5h)zvjM zH9vp;tgWrBtE;Q8uWx8*Xl!h3YHDh3ZfDz7Yinz7Z|~^n=gww5?(XU7 z>Fw?9>+9?9?;jW#7#tiN8XEfb>(}t`@W{x>=;-L!*x2~^_{7A-;?mO6^78V^%F62M>e|}c`uh5xKYunhHa0gmx3;#n zx3_n8c6N7n_xASo_xBM9#KFPA;o;%mzkiR8j*gFyPfkw${rh)%dU|$tc7A@2L?SOP zE-o)Gudc4Hudi=zZcr%H?d|Q|-QE5D{lnuz0syo(97d1(CIGCdruBIH|FeJupDWvd zKon3#8A)xArN7xXrZWmFL(&2K#ye^)PqFL+E7`(~3pOzceFbIek&+ac^`-23&O|u+ z0_pzuH)bY!HFk_gQ28@YV1HC?|7taT%x4e3qz_FULg5sq%V-*Ni~WJvhcT(r#55Ps zIKdGAQ!*tF3(aPK6^gpbe0#aCKY6tM?8>R9ef@VT3flXFK&lo zoP4V2j+*iLde3Fd52p;CGlL3VAK#8jqFhFwXIz-aZW>`Iv->vR5{VLKcEu^P+;dVA zCI_6B7bPmLU%x!%#PikTp-&K>Sq=LGiPiI_j~DmA2;^i>vXMQ0JvY`oIBwXYE)h+N*5}A@Hlt8 zwvinzKX^HAi&7CaC1N?;NS1c3fwSW|Sv+f23Qrqc-GxejpicaYx)*K`AIZmW4$59U zG`r|h-=P*;Ey^|29hCC2Ey3j95rU50?8cNl%JMZU*#*ONzSuuqUx(d0z-_#7S+BS{ zd+2_G9e#w8W#$(Ggo&@Dp04uUp&@q}H~TvHYGbHexnB;dXz7 zgIpRcfPXm2(D8z&tjdx*6X_lvCt~+Am7?E|7{V=X!F>O`U`w~P657`i)iS~+79K%N zGUJ}5fpQ9u&b*0NM4_u?Ir9;uL)ED{s^pAEsL0)Go3rLjVOJJ!S9jX&09oN!kMCkv zPRhAMYkc$HxO|jR^yZs>56$%VWKx@c-<_14`iIvj{@m?{BCEs0Njz@qkPMbvqCNeS z@JTL(E3~j;2}K-ginY&wBoITkt75$u{_tt{D=b!Ze+5%H-uB*E97J>B0FTT=ig+Ze zwwUzfKX-(8c!&+|WT*0wsyD^vfIFgDXkOa*j`n{4UH(7pTS=woN=CRZI6HpdG7~Y8 zOVhRYpCn@mhILz1XtGSb&SKkU9%LrkM^HcApcmdN4e-ml@^XD@ZPYF&K;#?|G>@qf zxS1gvH?0)%`km~b_iLJF80Tdaw4&^vsVnC7PhV2vKCjl`Y?r}pAH;Z~ye2lJ`~>3e z5nKIqcR^Gh!WB#95u^BopgdW`7A>a~H;@Td6hW|4^r_<@t=%335?wrPAsUE}CE{F`v z)uS`GJ9Rou76f~|i8>5sD&y!MHEzvg*_6Ck&<6IAKh55Rn!UqtPi>Xk!9;UFLzQCreS@F>mgZ<(=xjy^ zQ94trZWVm%iItUW+i77!;8IVuuVTS+#rDY5fT?32>FDx1#7wyH;)q*=llR`b6Y|e_ z*E$0p>L&B6DEW$C2d*-!YwaCWK4rgrPb8F&=RTi;uV+p7g-o01i00a-;`-PBZmFov*<8}MYS(2Jhj2wkAIXbryqm%kM z^9eUYglKsa6`t8I_dPe;KmL@Hk8#7eZ;NqN8O3@WBC_D+^Hh!tw3o?W-vH;s+RAid zidB0t?SjkdHf1JI;WSgvAS32abA|ZR-sac^>T+dP5ge3~7t^%Mv?2_kgb zLn@LZTZV#&YB@V`8nrS-`;qt7>o3}gks|#ZmKO!LKk>^OHg91*0keHJ}juq zP8VB{!kQDI;hHAfdX=guyZYDND{`914*pN1Tg)AffrJ{Z4>!M=(wOh%51B%8i9fB? ziS8NEBTjnSDZQu{?LH^MDbx{ij;|v~l)bdqcm)5Y+$pGFpD|Ul3;usdpLbtbtHw<`MC(K4h`V+|e?UqB@3rClx{ z`2K^+3}L_Ar=8WN`R-!urWVo0OE)E^bmU#-a>A!S;iy4YgkvwSVz`kfr7wT0j>}o< za&Ga%}&)v~m(+;?~9gV1pyQ63)*3}nwHP3to zkKsrJ>kAw33%cgV@4bui1~U$%*~QD`O!7r%yM*c~Yvz8FLaFe%=(esAY?GEhboZc7 zo}mMlx6|cDndqiJW?xlS!2bk;Exe5JvXdv83djc0_RfyEUM~}xhV^7_&8~=*{SKjp z$6VHI=Ygx7zim!CC+u!oNqe}iJ8#QHg7!I}=C(KGx6DQpnN0-LPF~-RD zy!K^=Tp&ekpk&XK;LHfa+O9De*=xF{bGu@S&aCnfoM=iB+SLVmHARRTMM@jk-{f+M zAu%HFn>xGMq0?`_>4&nC+sAlTa}0AyT^5!{SM~GP+Q{^ zuZ-M_IP^?$4t53^HTaRXn;c@4h6f6f2-{}vKjgRLN)kmAjj{9l5VxbDxcbis zxrSZ?uY+1{=t+@st`x;*dmBE68N8}9Dsnq$(^qW{$A3K?fOlFHZ~-4-bwp8#vRjyyn#wqr=Ip#l*? zUnHmN;1De&Ork{X>tz}Wo=HY6=-oMFKAA~A@wu^*Iz|MjC&(JuS*?3Fl_yIH9~x(l z!s^uDik4}jY3Oo03Ec{ELDPoAqWpHNvJCMNoG+l6F6F*KUAxm65e#KvsWUDQZ)FK) z%;q4^Z`0Dtk$zf1;CzV7?0MmCm|_x}wF`;~I$F>=(8?rO@W{WVoNeMYG4bt^(m>Sx zMCA65?&#Ld0r_r^8k~+~aeWcuVxV%tl@sbFpo4`NgpO#mGL`={-rBZhiE4mmeakW- zsS(H2Ak;LknzZjaW-}+Rxv%=dVCOh;+9e)dTM}ItbDLX*35%cL*R&b?g~*;HyDv64 z#p!HRlNh`he@7{CA>q+PQB(V{rzGuPwa4k3=!7#%;Bc68Yf-CMmg%ZzD#Rl0VEqzk z`oIh|b*VtOa?Z3l^t-yM`}S&!VY6`?O!v_jW&NsL9hpw~o*MW13MR7?3`6H+9ri$x z&i{R5inx$U%mnX<&(MZ>v!U>MiH*VzqGaKQW-Iomm%G7Ho!#J1ZqMFdus4dSx5@nW zj!Q##)T~8=yN4FEKd$4_UKAdRW_eHDA|2TMtvu*il<8|7VkfpY5n1tbf_yT}ErE;g zU(4l`Fs*O1{2oI#9|NB*oMb_K-3*U8#ieDg@?2^$^-|1}srDqKS#z98>&yHu^d%#r zX}R?WR9&Y4NvHM2+C~<4B0Y}Y4=HX|tK-%xAL_%4Ec&&}77AO78AB%7 zFyzd?g)U?(Qn1hU;yAfH^51oQjNNrdHh;u8Pq^IV^P+PeMkVUiLuq~n#Au|V!`dGHN+RskHHAV-)ke8t zA^Fp?(IC8yO6-a;2j)Oox8dJ6*ACgvUOTvneb?ujDKt9=jxu{Qm|pr5yls7)QgG$R z_{Rm&YkjQFJ6I2H=xFv+e6L!SYMCXzHPgW^He9=A)2Nl|K=IutHw~+gjQS0Sq}N9~#Bkx%WylV!|^@WP|K- zKCmXP)zxil1lA~Dz8=dKYM7FnkG6jnk+2ez_YDUb@YRsG~mHf5&h&vC{+2yX`oUIAqrQTX@m{8IN? zjvP|jB23tAZh^R!u?Pu*y)B8%AUo#L?RL2e@dMt8ZV8 zG07oD>PYf~&vk|kY>Oq^4ah2oWj<*X^Qh}gX1dsIKikqcl;&ZnW6!Q^@Z5hzf*^?= zGHo^zKGd0Vlx|I17%}suP2}IIDBk86J}ppQHxs+>U4>sfA8bh$(jbB5S9F|B+b61s zTXuYG&qu>}-C^ll)CRM6nX}Khlrj0;z?Nk5O)N6qXTfW%1-8RH3GY4zw)a z`7M6JXQgc!Hnc;(n@4VfYoOh$V=sUc<|s_|g0DmnTi95T3DY_`yEHiC>v-YYRnQQ-4UsH3=VPsWbm zJD)__2$nM=K7VnIEZN+17F_Y9&>=M8QOyQ-*3=c1(H|POrYcZ!9EUanUIzDD+igE! z{(q;b@8xxx+;^j+iDt5K!n38zo*L>2WBG33K#}9b%`c5Q^ zQy+C=M*#PKJU4Q1`hIqEVH3erMmJhp#Uf*fxJ8DIi=>OxLZ#gh=10ZAQpPT=Zy%#1 z${$+&7u^|LwQkUGfPV?c{V=|_Yrq+v+hW<8;lD>F8K}zBTXM3N;ehb^c(8aonCHHcPxG_SSNsw|MU5o zWm-x+V^6$nE2waSZ5y9cnkpN)-M(eqs5333AVFU1n^`RYwLhpU5Tv9~H!>E3MmSC= ztP5($C`_(2SJe|qqsn%cb-YBjqLCcSB!b>@t)(33+3uZ{`c)8WVvJ=%7GDvfFz{h| ztnN_uiEQP$dBj(FsDC&X1JuYbjPB;3=KO)^kV1!h=_DA5l3oo4Cpw0}VTBX-8^LaI zO8TcFR2@IH@TtWPy<`k-|Ky!(m8!VPSv2Lcu{N zm6dHe@P;KDKBnbWN0U}G17Do58*!axoDuRH^r@b8SM*_m<~IvnNPf5R6vjbtOT%uz z`3B`8G3e}sIaj}!|6?$i5PQsDI>$stCj@2`bB26XSQQd26W0HZPCqvj-lH#da!q=; zjkF_{Zf&3BGCgG*EswDqfOiC6#D(TF{%V)dCqLNT_Td~89){JsCO&}mQle;AwOz0L zGir5sJNU2W(`OofWMnALS%#D-=X$V3VT&iEKc>-ucegC6P7Plmitc`LB8xS)_M~E% z-La4}9tOC1WYo^ zx<;$%>R=GQU@V1Ce;CrfI20N_ei81EolQ(9*1V#KHp5l~*81%|SpgIzfF~MLj4et3 z=3Z`((T)CO+q>i|$++jt)!yk6pj;#o?`p5w#Lme=jE^T+;&+5vP8$FQKyFILJ6vMgm6_2 z5+266oVl6{rr{s5MyIjtNo=F}MJkllF-(S?Pr@lT3DieJ|UGfLj0ZwY!C~ zyS3UA{zj&6U*^sezGg_Z7M()6zU-PX+FL-vW31k=+!#HJHj7U5CPf9sd1H-!99GJ^ z@^av6@&HDMiDYN7Jd&AYoh~VJ{U_xSzNDsG%2z|OGy|%2!EjX`lPg5g<;GAo{@-DL zNg)r<@|cr1LV62bDPKA}OFK$;3_0%{8B1i=GfEB`Rc2r3uKAu_a#a!=?-W@2-xTN0 zcU?)??jF@m%0bY}uAbA_F}VL#5-5~ur`QK_a_uw7gyR2_J5&|-mTQTu6HL!ILP)|c zmpD`-{JmW+C(q;d1f*ZKjug|-?SK@+-CG;4mUhE{!&S#I6h>|c2e$bW8rc?61wAj? z)nZCu`1npib^mbP)K|6V5;ZiMaQZ0_y4{RGYlLN5YtvirDZ0_&mm2(z_}XTbqWCA~ zq?Hf9)US(DOS6%-^tnR&`8ExHDdLumAlt?`F{2EZnZI8AlNNb|49zSbV`rssV0X~d=m_O#v7;H;BQ_0YM(eR>dtrn5W60whOMNxftF;0irdPD|d+$*tAG)Yt zt(ilxkbJ#=ewqmr**@$2!=9rN_z=kD5yitYH6z{Mql}Ez3{)$;J+7R%eMQ8SEko5V z!vlK?GsVN?Jk8P!go~l|8r=)Bd_*B%(`=CU{zqB2sZp7bR?&P!b|g|8i-|B}G7SsMOU-s{%@Kq| zxb+W=M^oCwtJ#a-kzQ>s=cPj}*h4aJVysie&h!LB2GAK!%=njh5LS!mj$``Hh<{SH-(`uwpTw@ zzA5Gov`BLlI?)z`BkQ_XWgQ}RQ`L;{m;^Lr4WN|N&rfK4BpDD$4ym0SY7~`Kk{TZC zJeQOKjFL*9W5Y7Y0QHuemm@!hTX~4|-`BvUc=4c7L6e>`as-&-Tg6w8LY(D9wMpkj z1ZW}KEf%3?Q_iu*Ka*gW5q9j4#F`hHWF~j(I9s~ZGAse> zSM%=q88(BL0{MtiemhANh(c$+N6)j`_=v0DC={RbgEqpc7s7)j8XzPGc2g&Xh%r!qzN{i z12$Za#)Oy^iz#F$t09PLYpn)*a!kS+VKd}^O*vUxcT3OV%)ZfEO0^f?X1-QS{kxKg+GrfYsQ$zdOv?@U|#SFFPT-;dzraI06a6revk=To-?dvl;nMmzhXRe4tuNhw@xU2Y)j~e@~7Q2_TZO zt95$Xq&d$zKl&0#XuX4JDP>L!GSsBT^5j-S$ZVRwY_eu+D&>kl@M5243HtNZ3XGWG z`6H4wQOLOJBn(0OdMa#R5({?u*nJl{VOTP@`(HlZ^+TaB@wQhNDis_}SziT<289so zvrY5<3)p;mT^AR~9~BjG>x+)q9d}y}HhpHyfCLDq`&9lEC&2mf>>Q*ht0q$=_4&tt E1IoRQH2?qr literal 0 HcmV?d00001 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 26d9138..7ba2a0f 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -17,15 +17,13 @@ const defaultTitle = pageSeo.home.title; export const metadata: Metadata = { title: { default: defaultTitle, + template: "%s", }, description: pageSeo.home.description, metadataBase: new URL("https://modstagram.com"), keywords: siteKeywords, authors: [{ name: "Modstagram", url: "https://modstagram.com" }], creator: "Modstagram", - alternates: { - canonical: "/", - }, manifest: "/manifest.json", icons: { icon: [{ url: "/favicon.ico", type: "image/x-icon" }], @@ -40,19 +38,16 @@ export const metadata: Metadata = { statusBarStyle: "black", }, openGraph: { - title: defaultSEOConfig.openGraph?.title || defaultTitle, - description: defaultSEOConfig.openGraph?.description || "", - url: defaultSEOConfig.openGraph?.url || "", - siteName: defaultSEOConfig.openGraph?.site_name || "", + siteName: defaultSEOConfig.openGraph?.site_name || "مدستاگرام", locale: "fa_IR", type: "website", + images: defaultSEOConfig.openGraph?.images || [], }, robots: "index, follow", twitter: { card: "summary_large_image", - title: defaultTitle, - description: defaultSEOConfig.description || "", creator: "@modstagram", + site: "@modstagram", images: defaultSEOConfig.openGraph?.images?.map((img) => img.url) || [], }, verification: { diff --git a/src/app/offer/[id]/page.tsx b/src/app/offer/[id]/page.tsx index b506e06..aed9f06 100644 --- a/src/app/offer/[id]/page.tsx +++ b/src/app/offer/[id]/page.tsx @@ -4,6 +4,7 @@ import Container from "@/components/elements/Container"; import RoundedButton from "@/components/elements/RoundedButton"; import RoundedDiv from "@/components/elements/RoundedDiv"; +import { selectionCardClass } from "@/lib/ui/buttonStyles"; import { BASE_URL } from "@/components/main/BaseUrl"; import UserInfo from "@/components/main/UserInfo"; import PageTitle from "@/components/settings/PageTitle"; @@ -189,9 +190,9 @@ function TicketChat({ params }: ITicketChatProps) { {item.name === "normal" ? "درخواست همکاری" @@ -208,7 +209,7 @@ function TicketChat({ params }: ITicketChatProps) { ); })} - + ثبت درخواست diff --git a/src/app/offer/payment/failed/page.tsx b/src/app/offer/payment/failed/page.tsx index 0ea167d..8c9d30e 100644 --- a/src/app/offer/payment/failed/page.tsx +++ b/src/app/offer/payment/failed/page.tsx @@ -30,7 +30,7 @@ function FailedBillboard() { className="flex items-center flex-col mt-8" href={`/offer/${userId}`} > - + پرداخت مجدد diff --git a/src/app/offer/payment/success/page.tsx b/src/app/offer/payment/success/page.tsx index 3d9b877..ce3c460 100644 --- a/src/app/offer/payment/success/page.tsx +++ b/src/app/offer/payment/success/page.tsx @@ -47,12 +47,12 @@ function SuccessBillboard() {

- + ارسال پیام - + بعدا diff --git a/src/app/posts/[id]/[[...slug]]/page.tsx b/src/app/posts/[id]/[[...slug]]/page.tsx new file mode 100644 index 0000000..6eb3de1 --- /dev/null +++ b/src/app/posts/[id]/[[...slug]]/page.tsx @@ -0,0 +1,98 @@ +import { Metadata } from "next"; +import { fetchPostById } from "@/api/fetchPostById"; +import PostFeedView, { PostFeedSearchParams } from "@/components/posts/PostFeedView"; +import { buildStorageUrl } from "@/components/main/BaseUrl"; +import { generatePageMetadata } from "@/utils/generatePageMetadata"; +import { + buildPostPath, + buildPostPublicUrl, + buildPostSeoDescription, + buildPostSeoTitle, +} from "@/lib/postSlug"; + +type Props = { + params: Promise<{ id: string; slug?: string[] }>; + searchParams: Promise>; +}; + +function pickQuery( + sp: Record, + key: string +): string | undefined { + const v = sp[key]; + return typeof v === "string" ? v : undefined; +} + +function toFeedSearchParams( + sp: Record +): PostFeedSearchParams { + return { + feed: pickQuery(sp, "feed"), + tab: pickQuery(sp, "tab"), + userId: pickQuery(sp, "userId"), + expertise: pickQuery(sp, "expertise"), + province: pickQuery(sp, "province"), + city: pickQuery(sp, "city"), + userLevel: pickQuery(sp, "userLevel"), + rateFilter: pickQuery(sp, "rateFilter"), + hashtag: pickQuery(sp, "hashtag"), + exploreFilter: pickQuery(sp, "exploreFilter"), + subExpertise: pickQuery(sp, "subExpertise"), + filter: pickQuery(sp, "filter"), + lat: pickQuery(sp, "lat"), + lng: pickQuery(sp, "lng"), + q: pickQuery(sp, "q"), + }; +} + +export async function generateMetadata({ params }: Props): Promise { + const { id } = await params; + const post = await fetchPostById(id); + const title = post ? buildPostSeoTitle(post) : "پست | مدستاگرام"; + const description = post + ? buildPostSeoDescription(post) + : "مشاهده پست در مدستاگرام — پلتفرم تخصصی حوزه زیبایی، مدلینگ و عکاسی"; + const ogImage = post?.files?.[0]?.path + ? buildStorageUrl(post.files[0].path) + : undefined; + const authorName = + post && + ([post.first_name, post.last_name].filter(Boolean).join(" ") || + post.user_name); + + return generatePageMetadata({ + title, + description, + path: buildPostPath(id, post ?? undefined), + type: "article", + imageUrl: ogImage, + imageAlt: post?.caption?.slice(0, 80) || "پست مدستاگرام", + keywords: [ + "مدستاگرام", + post?.expertise || "", + authorName || "", + "پست", + "مدلینگ", + "زیبایی", + ].filter(Boolean), + publishedTime: post?.createdAt, + modifiedTime: post?.updatedAt, + }); +} + +/** بدون await روی API — نمایش فوری از sessionStorage در PostFeedView */ +export default async function PostPage({ params, searchParams }: Props) { + const { id } = await params; + const sp = await searchParams; + const feedSearchParams = toFeedSearchParams(sp); + + return ( +
+ +
+ ); +} diff --git a/src/app/posts/[id]/page.tsx b/src/app/posts/[id]/page.tsx deleted file mode 100644 index 5652437..0000000 --- a/src/app/posts/[id]/page.tsx +++ /dev/null @@ -1,69 +0,0 @@ -import { Metadata } from "next"; -import { fetchPostById } from "@/api/fetchPostById"; -import PostFeedView from "@/components/posts/PostFeedView"; -import { buildStorageUrl } from "@/components/main/BaseUrl"; -import { Suspense } from "react"; -import PageLoader from "@/components/ui/PageLoader"; -import { generatePageMetadata } from "@/utils/generatePageMetadata"; - -type Props = { params: Promise<{ id: string }> }; - -export async function generateMetadata({ params }: Props): Promise { - const { id } = await params; - const post = await fetchPostById(id); - const title = post?.caption - ? `${post.caption.slice(0, 60)} | مدستاگرام` - : "پست | مدستاگرام"; - const description = - post?.caption?.slice(0, 160) || - "مشاهده پست در مدستاگرام — پلتفرم تخصصی حوزه زیبایی، مدلینگ و عکاسی"; - const ogImage = post?.files?.[0]?.path - ? buildStorageUrl(post.files[0].path) - : undefined; - - return generatePageMetadata({ - title, - description, - path: `/posts/${id}`, - type: "article", - imageUrl: ogImage, - imageAlt: post?.caption?.slice(0, 80) || "پست مدستاگرام", - }); -} - -export default async function PostPage({ params }: Props) { - const { id } = await params; - const post = await fetchPostById(id); - const userId = post?.userId || post?.user_id; - - return ( -
- {post && ( -