diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000..0f6b4bb Binary files /dev/null and b/public/favicon.ico differ diff --git a/public/images/icons/192x192.png b/public/images/icons/192x192.png new file mode 100644 index 0000000..92ed3bd Binary files /dev/null and b/public/images/icons/192x192.png differ diff --git a/public/images/icons/actions-toggle.svg b/public/images/icons/actions-toggle.svg new file mode 100644 index 0000000..779d03b --- /dev/null +++ b/public/images/icons/actions-toggle.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/images/icons/apple-touch-icon.png b/public/images/icons/apple-touch-icon.png new file mode 100644 index 0000000..92ed3bd Binary files /dev/null and b/public/images/icons/apple-touch-icon.png differ diff --git a/public/images/icons/bookmark-post.svg b/public/images/icons/bookmark-post.svg new file mode 100644 index 0000000..d01a751 --- /dev/null +++ b/public/images/icons/bookmark-post.svg @@ -0,0 +1,3 @@ + + + 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 0000000..0f6b4bb Binary files /dev/null and b/src/app/icon.png differ 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 && ( -