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() {
-
+
اتاق کار
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("لینک آموزشگاه کپی شد");
+ }}
+ >
+
+ {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("mobile", academy.contactInfo)}
+ className="text-[12px] md:text-sm h-7 md:h-8 max-sm:text-[8px] rounded-xl border border-neutral-200 bg-white px-2 dark:border-neutral-700 dark:bg-neutral-900 active:opacity-80"
+ >
+ موبایل
+
+ openContact("whatsapp", academy.contactInfo)}
+ className="text-[12px] md:text-sm h-7 md:h-8 max-sm:text-[8px] rounded-xl border border-neutral-200 bg-white px-2 dark:border-neutral-700 dark:bg-neutral-900 active:opacity-80"
+ >
+ واتساپ
+
+ openContact("telegram", academy.contactInfo)}
+ className="text-[12px] md:text-sm h-7 md:h-8 max-sm:text-[8px] rounded-xl border border-neutral-200 bg-white px-2 dark:border-neutral-700 dark:bg-neutral-900 active:opacity-80"
+ >
+ تلگرام
+
+ 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 (
-
-
-
🏫
-
- آکادمی یافت نشد
-
-
- متأسفیم، آکادمی مورد نظر شما وجود ندارد یا حذف شده است.
-
-
router.push("/")}
- className="mt-6 px-6 py-2 bg-pink-500 text-white rounded-lg hover:bg-pink-600 transition-colors"
- >
- بازگشت به صفحه اصلی
-
-
-
- );
- }
-
- 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 */}
-
-
- {/* لوگو آکادمی - سمت راست در دسکتاپ */}
-
-
- {
- (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.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 && (
-
- )}
- }>
-
-
-
- );
-}
diff --git a/src/app/settings/academy/my-courses/page.tsx b/src/app/settings/academy/my-courses/page.tsx
index 01430f1..dded9d6 100644
--- a/src/app/settings/academy/my-courses/page.tsx
+++ b/src/app/settings/academy/my-courses/page.tsx
@@ -1,5 +1,7 @@
"use client";
+import { AcademyListSkeleton } from "@/components/academy/AcademySkeletons";
+
import useAxios from "@/hooks/useAxios";
import React, { useEffect, useState, useCallback } from "react";
import MainModelCard from "@/components/academy/MainModelCard";
@@ -80,16 +82,8 @@ export default function PurchasedCoursesPage() {
if (isLoading) {
return (
-
-
-
-
- در حال بارگذاری دورههای شما...
-
-
+
);
}
diff --git a/src/app/settings/academy/storeProfile/page.tsx b/src/app/settings/academy/storeProfile/page.tsx
index 03197c5..e7d385d 100644
--- a/src/app/settings/academy/storeProfile/page.tsx
+++ b/src/app/settings/academy/storeProfile/page.tsx
@@ -29,6 +29,8 @@ import { useRouter } from "next/navigation";
import useAxios from "@/hooks/useAxios";
import { Academy } from "@/types/types";
import { IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
+import { AcademyProfileHeadSkeleton } from "@/components/academy/AcademySkeletons";
+import { Skeleton } from "@/components/ui/skeleton";
// Zod schema for store settings
const storeSchema = z.object({
@@ -197,8 +199,13 @@ export default function StoreSettings() {
// نمایش لودینگ در حین دریافت دیتا
if (isFetching) {
return (
-
-
+
);
}
diff --git a/src/app/settings/academy/wallet/page.tsx b/src/app/settings/academy/wallet/page.tsx
index a82e9a4..12dab6d 100644
--- a/src/app/settings/academy/wallet/page.tsx
+++ b/src/app/settings/academy/wallet/page.tsx
@@ -2,6 +2,7 @@
import React, { useState, useEffect } from "react";
import { ThemeProvider, useTheme } from "next-themes";
+import { Skeleton } from "@/components/ui/skeleton";
import {
Card,
@@ -211,8 +212,13 @@ const WalletDashboard = () => {
if (isLoading) {
return (
-
-
+
);
}
diff --git a/src/app/settings/chats/[username]/[id]/page.tsx b/src/app/settings/chats/[username]/[id]/page.tsx
index e74a57a..9f7eac3 100644
--- a/src/app/settings/chats/[username]/[id]/page.tsx
+++ b/src/app/settings/chats/[username]/[id]/page.tsx
@@ -10,6 +10,7 @@ import {
} from "@/lib/chat/socketClient";
import MessageInput from "@/components/chat/MessageInput";
import MultiImageModal from "@/components/chat/MultiImageModal";
+import MediaPreviewModal from "@/components/chat/MediaPreviewModal";
import ChatActionBar from "@/components/chat/ChatActionBar";
import ChatDeleteBar from "@/components/chat/ChatDeleteBar";
import { useUser } from "@/hooks/useUser";
@@ -26,7 +27,6 @@ import { chatThreadQueryKey } from "@/lib/chat/queryKeys";
import { normalizeThreadId, upsertMessageInThreadCache } from "@/lib/chat/threadCache";
import { useStableMessageKeys } from "@/hooks/useStableMessageKeys";
import { getStoredUserId } from "@/lib/auth/session";
-import { buildExpiresAtIso } from "@/lib/chat/timedMessages";
import { isViewOnceMediaType } from "@/lib/chat/viewOnce";
import {
findPendingMatchForServer,
@@ -52,6 +52,10 @@ function TicketChat({ params }: ITicketChatProps) {
const [userTwoDetail, setUserTwoDetail] = useState
();
const [pendingImages, setPendingImages] = useState([]);
const [showMultiModal, setShowMultiModal] = useState(false);
+ const [pendingMediaPreview, setPendingMediaPreview] = useState<{
+ file: File;
+ kind: "video" | "file";
+ } | null>(null);
const [pendingMessages, setPendingMessages] = useState([]);
const [replyingTo, setReplyingTo] = useState(null);
const [forwardMessage, setForwardMessage] = useState(null);
@@ -170,12 +174,12 @@ function TicketChat({ params }: ITicketChatProps) {
);
if (videos.length === 1 && images.length === 0 && others.length === 0) {
- void processSendMessage(videos[0], "video");
+ setPendingMediaPreview({ file: videos[0], kind: "video" });
return;
}
if (others.length === 1 && images.length === 0 && videos.length === 0) {
- void processSendMessage(others[0], "file");
+ setPendingMediaPreview({ file: others[0], kind: "file" });
return;
}
@@ -248,6 +252,11 @@ function TicketChat({ params }: ITicketChatProps) {
let textContent = contentOverride ?? newMessage;
if (textContent.trim() === "" && !fileToUpload) return;
+ if (userTwoDetail?.blocked_you || userTwoDetail?.is_blocked) {
+ toast.error("امکان ارسال پیام وجود ندارد.");
+ return;
+ }
+
const tempId = `temp-${Date.now()}-${Math.random()}`;
const replyPayload = buildReplyPayload(replySource);
@@ -265,7 +274,7 @@ function TicketChat({ params }: ITicketChatProps) {
senderId: currentUserId,
receiverId: targetReceiverId,
createdAt: new Date().toISOString(),
- expiresAt: buildExpiresAtIso(selfDestructSeconds),
+ selfDestructSeconds: selfDestructSeconds || undefined,
status: "pending",
file: fileToUpload ? URL.createObjectURL(fileToUpload) : "",
fileType: fileType,
@@ -293,7 +302,6 @@ function TicketChat({ params }: ITicketChatProps) {
let messagePayload: Record = {
content: textContent,
receiverId: targetReceiverId,
- senderId: currentUserId,
};
if (replySource?._id && !replySource._id.startsWith("temp")) {
@@ -425,7 +433,7 @@ function TicketChat({ params }: ITicketChatProps) {
queryClient.setQueryData(
chatThreadQueryKey(
normalizeThreadId(user._id),
- normalizeThreadId(chatPartnerId)
+ normalizeThreadId(userTwoDetail?._id ?? receiverId ?? chatPartnerId)
),
(oldData: { pages: { messages: ChatMessage[] }[] } | undefined) => {
if (!oldData) return oldData;
@@ -511,7 +519,7 @@ function TicketChat({ params }: ITicketChatProps) {
onLocationShare={handleLocationShare}
onTyping={handleTyping}
isChatThread
- blocked_you={userTwoDetail?.blocked_you}
+ blocked_you={userTwoDetail?.blocked_you || userTwoDetail?.is_blocked}
replyingTo={replyingTo}
onCancelReply={() => {
setReplyingTo(null);
@@ -550,6 +558,22 @@ function TicketChat({ params }: ITicketChatProps) {
selfDestructSeconds={selfDestructSeconds}
onSelfDestructChange={setSelfDestructSeconds}
/>
+ setPendingMediaPreview(null)}
+ onConfirm={() => {
+ if (!pendingMediaPreview) return;
+ const { file, kind } = pendingMediaPreview;
+ setPendingMediaPreview(null);
+ void processSendMessage(file, kind === "video" ? "video" : "file");
+ }}
+ viewOnceMedia={viewOnceMedia}
+ onViewOnceChange={setViewOnceMedia}
+ selfDestructSeconds={selfDestructSeconds}
+ onSelfDestructChange={setSelfDestructSeconds}
+ />
{forwardMessage && user?._id && (
)}
-
- {item.first_name} {item.last_name}
+
+ {item.display_name ||
+ `${item.first_name} ${item.last_name}`.trim()}
+
{item.user_name}
-
{item.unread_messages_count ? (
{item.unread_messages_count}
diff --git a/src/app/settings/edit/colors/page.tsx b/src/app/settings/edit/colors/page.tsx
index 9964c10..b588971 100644
--- a/src/app/settings/edit/colors/page.tsx
+++ b/src/app/settings/edit/colors/page.tsx
@@ -93,7 +93,7 @@ function Colors() {
handleButtonPress(1)}
@@ -103,7 +103,7 @@ function Colors() {
handleButtonPress(2)}
diff --git a/src/app/settings/edit/cooperation-type/page.tsx b/src/app/settings/edit/cooperation-type/page.tsx
index 1035ff3..af54b47 100644
--- a/src/app/settings/edit/cooperation-type/page.tsx
+++ b/src/app/settings/edit/cooperation-type/page.tsx
@@ -63,7 +63,7 @@ function CooperationType() {
onClick={() => formik.setFieldValue("selectedButton", true)}
className={`mt-5 text-xs w-full max-w-[250px] flex items-center justify-center flex-row-reverse gap-2 px-1 ${
formik.values.selectedButton === true
- ? "!bg-[#FC8EAC] !border-[#FC8EAC] !text-white"
+ ? "btn-modern--selected text-white"
: ""
}`}
>
@@ -74,7 +74,7 @@ function CooperationType() {
onClick={() => formik.setFieldValue("selectedButton", false)}
className={`mt-5 text-xs w-full max-w-[250px] flex items-center justify-center flex-row-reverse gap-2 px-1 ${
formik.values.selectedButton === false
- ? "!bg-[#FC8EAC] !border-[#FC8EAC] !text-white"
+ ? "btn-modern--selected text-white"
: ""
}`}
>
diff --git a/src/app/settings/edit/layout.tsx b/src/app/settings/edit/layout.tsx
new file mode 100644
index 0000000..40a9cee
--- /dev/null
+++ b/src/app/settings/edit/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "ویرایش پروفایل | مدستاگرام",
+ description: "ویرایش اطلاعات و تنظیمات پروفایل در مدستاگرام",
+ path: "/settings/edit",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/edit/public-relations/page.tsx b/src/app/settings/edit/public-relations/page.tsx
index 4827204..c499591 100644
--- a/src/app/settings/edit/public-relations/page.tsx
+++ b/src/app/settings/edit/public-relations/page.tsx
@@ -69,7 +69,7 @@ function PublicRelations() {
onClick={() => formik.setFieldValue("selectedButton", true)}
className={`mt-5 text-xs w-full max-w-[250px] flex items-center justify-center flex-row-reverse gap-2 px-1 ${
formik.values.selectedButton === true
- ? "!bg-[#FC8EAC] !border-[#FC8EAC] !text-white"
+ ? "btn-modern--selected text-white"
: ""
}`}
>
@@ -80,7 +80,7 @@ function PublicRelations() {
onClick={() => formik.setFieldValue("selectedButton", false)}
className={`mt-5 text-xs w-full max-w-[250px] flex items-center justify-center flex-row-reverse gap-2 px-1 ${
formik.values.selectedButton === false
- ? "!bg-[#FC8EAC] !border-[#FC8EAC] !text-white"
+ ? "btn-modern--selected text-white"
: ""
}`}
>
diff --git a/src/app/settings/edit/sizes/page.tsx b/src/app/settings/edit/sizes/page.tsx
index 3473966..921436c 100644
--- a/src/app/settings/edit/sizes/page.tsx
+++ b/src/app/settings/edit/sizes/page.tsx
@@ -82,7 +82,7 @@ function Sizes() {
handleButtonPress(1)}
@@ -99,7 +99,7 @@ function Sizes() {
handleButtonPress(2)}
@@ -116,7 +116,7 @@ function Sizes() {
handleButtonPress(3)}
@@ -159,7 +159,7 @@ function Sizes() {
key={sizeOption}
className={`p-2 py-1 rounded-full border text-sm full ${
size === sizeOption
- ? "bg-[#FC8EAC] text-white border-[#FC8EAC]"
+ ? "btn-modern--selected text-white"
: "bg-tertiary-light dark:bg-tertiary-dark border-gray-400"
}`}
onClick={() => handleSizeSelection(sizeOption)}
diff --git a/src/app/settings/favorites/page.tsx b/src/app/settings/favorites/page.tsx
new file mode 100644
index 0000000..f3ee99a
--- /dev/null
+++ b/src/app/settings/favorites/page.tsx
@@ -0,0 +1,20 @@
+import Container from "@/components/elements/Container";
+import PageTitle from "@/components/settings/PageTitle";
+import FavoritesGrid from "@/components/settings/FavoritesGrid";
+import { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "علاقمندیها | مدستاگرام",
+ description: "پستهای ذخیرهشده در علاقمندیهای شما در مدستاگرام",
+ path: "/settings/favorites",
+});
+
+export default function FavoritesPage() {
+ return (
+
+ علاقمندیها
+
+
+ );
+}
diff --git a/src/app/settings/financial/layout.tsx b/src/app/settings/financial/layout.tsx
new file mode 100644
index 0000000..ac4867b
--- /dev/null
+++ b/src/app/settings/financial/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "امور مالی | مدستاگرام",
+ description: "تراکنشها و امور مالی حساب کاربری در مدستاگرام",
+ path: "/settings/financial",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/layout.tsx b/src/app/settings/layout.tsx
index f159beb..a72f739 100644
--- a/src/app/settings/layout.tsx
+++ b/src/app/settings/layout.tsx
@@ -3,8 +3,7 @@ import type { Metadata } from "next";
export const metadata: Metadata = {
title: {
- default: "تنظیمات",
- template: "%s | مدستاگرام",
+ default: "تنظیمات | مدستاگرام",
},
description: "تنظیمات حساب کاربری در مدستاگرام",
robots: { index: false, follow: false },
diff --git a/src/app/settings/my-billboards/layout.tsx b/src/app/settings/my-billboards/layout.tsx
new file mode 100644
index 0000000..cde0256
--- /dev/null
+++ b/src/app/settings/my-billboards/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "بیلبوردهای من | مدستاگرام",
+ description: "مدیریت بیلبوردهای تبلیغاتی شما در مدستاگرام",
+ path: "/settings/my-billboards",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/notifications/layout.tsx b/src/app/settings/notifications/layout.tsx
new file mode 100644
index 0000000..fdd5101
--- /dev/null
+++ b/src/app/settings/notifications/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "اعلانها | مدستاگرام",
+ description: "اعلانها و پیامهای سیستمی حساب کاربری در مدستاگرام",
+ path: "/settings/notifications",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/notifications/page.tsx b/src/app/settings/notifications/page.tsx
index 6bc2ad6..46e4839 100644
--- a/src/app/settings/notifications/page.tsx
+++ b/src/app/settings/notifications/page.tsx
@@ -9,6 +9,50 @@ import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useState } from "react";
+const LIKE_NOTIFICATION_TYPES = new Set([
+ "post_like",
+ "academy_like",
+ "billboard_like",
+]);
+
+const COMMENT_NOTIFICATION_TYPES = new Set([
+ "post_comment",
+ "profile_comment",
+ "user_comment",
+ "academy_comment",
+ "billboard_comment",
+ "vitrine-comment",
+]);
+
+const RATING_NOTIFICATION_TYPES = new Set([
+ "post_rating",
+ "profile_rating",
+ "academy_rating",
+ "billboard_rating",
+]);
+
+function getNotificationTitle(item: INotification): string {
+ if (LIKE_NOTIFICATION_TYPES.has(item.type)) return "پست شما لایک شد";
+ if (COMMENT_NOTIFICATION_TYPES.has(item.type)) {
+ return "یک کامنت برای شما ثبت شد";
+ }
+ if (RATING_NOTIFICATION_TYPES.has(item.type)) {
+ return "یک امتیاز برای شما ثبت شد";
+ }
+ return item.title;
+}
+
+function getNotificationDescription(item: INotification): string {
+ if (LIKE_NOTIFICATION_TYPES.has(item.type)) return "پست شما لایک شد";
+ if (COMMENT_NOTIFICATION_TYPES.has(item.type)) {
+ return "یک کامنت برای شما ثبت شد";
+ }
+ if (RATING_NOTIFICATION_TYPES.has(item.type)) {
+ return "یک امتیاز برای شما ثبت شد";
+ }
+ return item.description;
+}
+
export interface INotification {
_id: string;
userId: string;
@@ -61,9 +105,14 @@ function Notifications() {
academy_like: `/academy/${item?.project_post_id}/course`,
billboard_like: `/settings/my-billboards/${item?.project_post_id}/b`,
post_comment: `/posts/${item?.project_post_id}`,
+ post_rating: `/posts/${item?.project_post_id}`,
+ post_tag: `/posts/${item?.project_post_id}`,
profile_comment: "/settings/profile",
+ profile_rating: "/settings/profile",
user_comment: "/settings/profile",
academy_comment: `/academy/${item?.project_post_id}/course`,
+ academy_rating: `/academy/${item?.project_post_id}/course`,
+ academy_purchase: `/academy/${item?.project_post_id}/course`,
billboard_comment: `/settings/my-billboards/${item?.project_post_id}/b`,
billboard_rating: `/settings/my-billboards/${item?.project_post_id}/b`,
"reject-user": `/tickets/new/${
@@ -115,11 +164,11 @@ function Notifications() {
) : (
""
)}
- {item?.title}
+ {getNotificationTitle(item)}
{item?.createdAt}
-
{item?.description}
+
{getNotificationDescription(item)}
))}
diff --git a/src/app/settings/offers/layout.tsx b/src/app/settings/offers/layout.tsx
new file mode 100644
index 0000000..bbeade2
--- /dev/null
+++ b/src/app/settings/offers/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "درخواستهای همکاری | مدستاگرام",
+ description: "مدیریت درخواستها و پیشنهادهای همکاری در مدستاگرام",
+ path: "/settings/offers",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/offers/page.tsx b/src/app/settings/offers/page.tsx
index 9aae3fa..60272c7 100644
--- a/src/app/settings/offers/page.tsx
+++ b/src/app/settings/offers/page.tsx
@@ -10,6 +10,8 @@ import UserDetails from "@/components/settings/UserDetails";
import useInfiniteScroll from "@/hooks/useInfiniteScroll";
import { IOffers } from "@/types/types";
import React, { useState } from "react";
+import { toggleBtnClass } from "@/lib/ui/buttonStyles";
+import { cn } from "@/lib/utils";
function Offers() {
const [filter, setFilter] = useState
("درخواست");
@@ -41,17 +43,13 @@ function Offers() {
setFilter("درخواست")}
>
درخواست
setFilter("دریافت")}
>
دریافت
@@ -68,6 +66,7 @@ function Offers() {
))}
@@ -88,6 +87,7 @@ function Offers() {
isOpen={showConfirmModal}
onClose={() => setShowConfirmModal(false)}
item={itemToAction}
+ mode="received"
refetch={refetch}
/>
)}
diff --git a/src/app/settings/page.tsx b/src/app/settings/page.tsx
index 77e9b4b..96d4dbb 100644
--- a/src/app/settings/page.tsx
+++ b/src/app/settings/page.tsx
@@ -9,6 +9,14 @@ import { staticIconUrl } from "@/components/main/BaseUrl";
import Image from "next/image";
import Link from "next/link";
import React from "react";
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "تنظیمات | مدستاگرام",
+ description: "تنظیمات حساب کاربری در مدستاگرام",
+ path: "/settings",
+});
function Settings() {
return (
diff --git a/src/app/settings/profile/ProfileClient.tsx b/src/app/settings/profile/ProfileClient.tsx
new file mode 100644
index 0000000..8434cb7
--- /dev/null
+++ b/src/app/settings/profile/ProfileClient.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Container from "@/components/elements/Container";
+import ModelHead from "@/components/models/ModelPage/ModelHead";
+import useAxios from "@/hooks/useAxios";
+import ModelContent from "@/components/models/ModelPage/ModelContent";
+import { User } from "@/types/types";
+
+export default function ProfileClient() {
+ const [user, setUser] = useState(null);
+ const { request, loading } = useAxios();
+
+ const fetchUser = async () => {
+ try {
+ const response = await request<{ user: User }>("GET", "/profile");
+ setUser(response?.user ?? null);
+ } catch (err) {
+ console.error("خطا در دریافت اطلاعات کاربر:", err);
+ }
+ };
+
+ useEffect(() => {
+ fetchUser();
+ }, []);
+
+ if (loading || !user) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+ );
+}
diff --git a/src/app/settings/profile/info/page.tsx b/src/app/settings/profile/info/page.tsx
index 81a1082..2203725 100644
--- a/src/app/settings/profile/info/page.tsx
+++ b/src/app/settings/profile/info/page.tsx
@@ -24,13 +24,13 @@ function ProfileInfo() {
-
+
);
}
diff --git a/src/app/settings/profile/layout.tsx b/src/app/settings/profile/layout.tsx
index 5288b90..93ebe70 100644
--- a/src/app/settings/profile/layout.tsx
+++ b/src/app/settings/profile/layout.tsx
@@ -1,21 +1,7 @@
-export const metadata = {
- title: "پروفایل کاربر - مدستاگرام",
- description: "مشاهده بیو، تخصص، و نمونه کارهای کاربر در پلتفرم مدستاگرام.",
- other: {
- "script:type": "application/ld+json",
- "script:data": JSON.stringify({
- "@context": "https://schema.org",
- "@type": "Person",
- "name": "نام کاربر",
- "jobTitle": "تخصص کاربر",
- "description": "بیوگرافی، تخصص و سطح فعالیت در مدستاگرام.",
- "image": "https://modstagram.com/images/user-avatar.jpg",
- "inLanguage": "fa"
- })
- }
- };
-
- export default function ProfileLayout({ children }: { children: React.ReactNode }) {
- return <>{children}>;
- }
-
\ No newline at end of file
+export default function ProfileLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ return <>{children}>;
+}
diff --git a/src/app/settings/profile/page.tsx b/src/app/settings/profile/page.tsx
index 0084824..68089c9 100644
--- a/src/app/settings/profile/page.tsx
+++ b/src/app/settings/profile/page.tsx
@@ -1,57 +1,52 @@
-"use client";
-
-import React, { useEffect, useState } from "react";
+import { cookies } from "next/headers";
+import { Metadata } from "next";
+import ProfileClient from "./ProfileClient";
+import { BASE_URL } from "@/components/main/BaseUrl";
import { User } from "@/types/types";
-import Container from "@/components/elements/Container";
-import ModelHead from "@/components/models/ModelPage/ModelHead";
-import useAxios from "@/hooks/useAxios";
-import ModelContent from "@/components/models/ModelPage/ModelContent";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
-
-
-
-
-function Profile() {
- const [user, setUser] = useState(null);
- const { request, loading } = useAxios();
-
- const fetchUser = async () => {
- try {
- const response = await request<{ user: User }>("GET", "/profile");
- setUser(response?.user ?? null);
- } catch (err) {
- console.error("خطا در دریافت اطلاعات کاربر:", err);
+export async function generateMetadata(): Promise {
+ const token = (await cookies()).get("token")?.value || "";
+ try {
+ const res = await fetch(`${BASE_URL}/profile`, {
+ cache: "no-store",
+ headers: { Authorization: token ? `Bearer ${token}` : "" },
+ });
+ const data = await res.json();
+ const user = data?.user as User;
+ if (!user) {
+ return generatePageMetadata({
+ title: "پروفایل | مدستاگرام",
+ description: "پروفایل کاربری در مدستاگرام",
+ path: "/settings/profile",
+ });
}
- };
+ const fullName = `${user.first_name || ""} ${user.last_name || ""}`.trim();
+ const expertise = user.expertise || "";
+ const city = user.city?.name || "";
+ const bio = user.bio || "";
+ const username = user.user_name || "";
- useEffect(() => {
- fetchUser();
- }, []);
+ const title = [username, fullName, expertise, city, "مدستاگرام"]
+ .filter(Boolean)
+ .join(" – ");
+ const description = [fullName, expertise, bio].filter(Boolean).join(" – ");
- // لودینگ
- if (loading || !user) {
- return (
-
-
-
- );
+ return generatePageMetadata({
+ title,
+ description,
+ path: "/settings/profile",
+ type: "profile",
+ });
+ } catch {
+ return generatePageMetadata({
+ title: "پروفایل | مدستاگرام",
+ description: "پروفایل کاربری در مدستاگرام",
+ path: "/settings/profile",
+ });
}
-
- return (
-
-
-
-
-
-
- );
}
-export default Profile;
+export default function ProfilePage() {
+ return ;
+}
diff --git a/src/app/settings/profile/user-settings/layout.tsx b/src/app/settings/profile/user-settings/layout.tsx
new file mode 100644
index 0000000..6d89d7f
--- /dev/null
+++ b/src/app/settings/profile/user-settings/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "تنظیمات پروفایل | مدستاگرام",
+ description: "تنظیمات نمایش و حریم خصوصی پروفایل در مدستاگرام",
+ path: "/settings/profile/user-settings",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/profile/user-settings/page.tsx b/src/app/settings/profile/user-settings/page.tsx
new file mode 100644
index 0000000..a4ab111
--- /dev/null
+++ b/src/app/settings/profile/user-settings/page.tsx
@@ -0,0 +1,144 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Container from "@/components/elements/Container";
+import PageTitle from "@/components/settings/PageTitle";
+import useAxios from "@/hooks/useAxios";
+import Link from "next/link";
+import toast from "react-hot-toast";
+
+export default function UserSettingsPage() {
+ const { request } = useAxios();
+ const [allowSave, setAllowSave] = useState(true);
+ const [ghostMode, setGhostMode] = useState(false);
+ const [loading, setLoading] = useState(true);
+ const [accountStatus, setAccountStatus] = useState("active");
+ const [reactivationLockedUntil, setReactivationLockedUntil] = useState(null);
+ const [reactivating, setReactivating] = useState(false);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await request<{
+ account: {
+ allow_save_posts?: boolean;
+ ghost_mode?: boolean;
+ account_status?: string;
+ reactivation_locked_until?: string;
+ };
+ }>("GET", "/account/status", null, { noToast: true });
+ setAllowSave(res?.account?.allow_save_posts !== false);
+ setGhostMode(Boolean(res?.account?.ghost_mode));
+ setAccountStatus(res?.account?.account_status || "active");
+ setReactivationLockedUntil(res?.account?.reactivation_locked_until || null);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [request]);
+
+ const savePrivacy = async (patch: { allow_save_posts?: boolean; ghost_mode?: boolean }) => {
+ try {
+ const res = await request<{ allow_save_posts: boolean; ghost_mode: boolean }>(
+ "PATCH",
+ "/account/privacy",
+ patch,
+ { noToast: true }
+ );
+ setAllowSave(res.allow_save_posts);
+ setGhostMode(res.ghost_mode);
+ toast.success("ذخیره شد");
+ } catch {
+ toast.error("خطا در ذخیره");
+ }
+ };
+
+ const reactivate = async () => {
+ setReactivating(true);
+ try {
+ await request("POST", "/account/reactivate", {});
+ setAccountStatus("active");
+ toast.success("حساب کاربری فعال شد");
+ } catch (err) {
+ toast.error(err instanceof Error ? err.message : "امکان فعالسازی نیست");
+ } finally {
+ setReactivating(false);
+ }
+ };
+
+ const lockActive =
+ accountStatus === "deactivated" &&
+ reactivationLockedUntil &&
+ new Date(reactivationLockedUntil) > new Date();
+
+ return (
+
+ تنظیمات کاربر
+
+ {accountStatus === "deactivated" && (
+
+
+ حساب شما غیرفعال است
+
+ {lockActive ? (
+
+ تا{" "}
+ {new Date(reactivationLockedUntil!).toLocaleDateString("fa-IR")}{" "}
+ امکان فعالسازی مجدد وجود ندارد.
+
+ ) : (
+
+ {reactivating ? "…" : "فعالسازی مجدد حساب"}
+
+ )}
+
+ )}
+
+
+
بازدیدکنندگان
+
›
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/settings/profile/visitors/layout.tsx b/src/app/settings/profile/visitors/layout.tsx
new file mode 100644
index 0000000..33f3d73
--- /dev/null
+++ b/src/app/settings/profile/visitors/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "بازدیدکنندگان پروفایل | مدستاگرام",
+ description: "لیست بازدیدکنندگان پروفایل شما در مدستاگرام",
+ path: "/settings/profile/visitors",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/profile/visitors/page.tsx b/src/app/settings/profile/visitors/page.tsx
new file mode 100644
index 0000000..7ff8089
--- /dev/null
+++ b/src/app/settings/profile/visitors/page.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import React, { useEffect, useState } from "react";
+import Container from "@/components/elements/Container";
+import PageTitle from "@/components/settings/PageTitle";
+import useAxios from "@/hooks/useAxios";
+import Image from "next/image";
+import Link from "next/link";
+import { buildStorageUrl } from "@/components/main/BaseUrl";
+
+interface Visitor {
+ _id: string;
+ user_name: string;
+ first_name: string;
+ last_name: string;
+ profile_image?: string;
+ visited_at?: string;
+}
+
+export default function ProfileVisitorsPage() {
+ const { request } = useAxios();
+ const [visitors, setVisitors] = useState([]);
+ const [loading, setLoading] = useState(true);
+
+ useEffect(() => {
+ (async () => {
+ try {
+ const res = await request<{ visitors: Visitor[] }>(
+ "GET",
+ "/account/visitors?limit=50",
+ null,
+ { noToast: true }
+ );
+ setVisitors(res?.visitors || []);
+ } finally {
+ setLoading(false);
+ }
+ })();
+ }, [request]);
+
+ return (
+
+ بازدیدکنندگان پروفایل
+
+ {loading ? (
+
در حال بارگذاری…
+ ) : visitors.length === 0 ? (
+
+ هنوز بازدیدی ثبت نشده
+
+ ) : (
+
+ {visitors.map((v) => {
+ const name =
+ [v.first_name, v.last_name].filter(Boolean).join(" ") || v.user_name;
+ return (
+ -
+
+
+
+
+
+
{name}
+
@{v.user_name}
+
+
+
+ );
+ })}
+
+ )}
+
+
+ );
+}
diff --git a/src/app/settings/tickets/layout.tsx b/src/app/settings/tickets/layout.tsx
new file mode 100644
index 0000000..9b350a1
--- /dev/null
+++ b/src/app/settings/tickets/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "تیکتهای پشتیبانی | مدستاگرام",
+ description: "ارسال و پیگیری تیکتهای پشتیبانی مدستاگرام",
+ path: "/settings/tickets",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/settings/workroom/layout.tsx b/src/app/settings/workroom/layout.tsx
new file mode 100644
index 0000000..6b1dc7f
--- /dev/null
+++ b/src/app/settings/workroom/layout.tsx
@@ -0,0 +1,12 @@
+import type { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+
+export const metadata: Metadata = generatePageMetadata({
+ title: "اتاق کار | مدستاگرام",
+ description: "مدیریت پروژهها و اتاق کار در مدستاگرام",
+ path: "/settings/workroom",
+});
+
+export default function Layout({ children }: { children: React.ReactNode }) {
+ return children;
+}
diff --git a/src/app/sitemap.ts b/src/app/sitemap.ts
index 0f28c8b..eb9d655 100644
--- a/src/app/sitemap.ts
+++ b/src/app/sitemap.ts
@@ -1,9 +1,23 @@
import { MetadataRoute } from 'next'
+const API_BASE = 'https://api.modstagram.ir/api/v1'
+
+async function fetchJsonSafe(url: string): Promise {
+ try {
+ const res = await fetch(url, { next: { revalidate: 3600 } })
+ const contentType = res.headers.get('content-type') || ''
+ if (!res.ok || !contentType.includes('application/json')) {
+ return null
+ }
+ return (await res.json()) as T
+ } catch {
+ return null
+ }
+}
+
export default async function sitemap(): Promise {
const baseUrl = 'https://modstagram.com'
- // ۱. صفحات اصلی و کلیدی
const staticRoutes = ['', '/videos', '/billboards', '/users', '/posts'].map((route) => ({
url: `${baseUrl}${route}`,
lastModified: new Date(),
@@ -11,86 +25,42 @@ export default async function sitemap(): Promise {
priority: route === '' ? 1.0 : 0.9,
}))
- try {
- // ۲. واکشی بیلبوردها (هماهنگ با منطق جدید URL اسلاگ)
- const billboardsRes = await fetch('https://api.modstagram.ir/advertising/get/all/web', {
- next: { revalidate: 3600 }
- })
- const billboardData = await billboardsRes.json()
- const billboards = billboardData?.advertisings || []
-
- const billboardRoutes = billboards.map((b: any) => {
- // ساخت اسلاگ دقیقاً مشابه منطق استفاده شده در صفحه جزییات بیلبورد
- const titleParts = [
- b.title,
- b.category,
- b.province?.name,
- b.city?.name,
- b.neighbourhood !== b.city?.name ? b.neighbourhood : null,
- ].filter(Boolean);
-
- const slug = titleParts.join(" ").trim().replace(/\s+/g, '-').replace(/-+/g, '-');
+ const billboardData = await fetchJsonSafe<{ advertisings?: Array> }>(
+ `${API_BASE}/advertising/web?page=1&limit=200`
+ )
+ const billboards = billboardData?.advertisings || []
- return {
- url: `${baseUrl}/billboards/${b._id}/${encodeURIComponent(slug)}`,
- lastModified: new Date(b.updatedAt || new Date()),
- priority: 0.8,
- }
- })
+ const billboardRoutes = billboards.map((b) => {
+ const titleParts = [
+ b.title,
+ b.category,
+ (b.province as { name?: string } | undefined)?.name,
+ (b.city as { name?: string } | undefined)?.name,
+ b.neighbourhood !== (b.city as { name?: string } | undefined)?.name ? b.neighbourhood : null,
+ ].filter(Boolean)
- // ۳. واکشی کاربران برای ایندکس شدن پروفایلها
- const usersRes = await fetch('https://api.modstagram.ir/users/get/all/web', {
- next: { revalidate: 3600 }
- })
- const userData = await usersRes.json()
- const users = userData?.users || []
+ const slug = String(titleParts.join(' '))
+ .trim()
+ .replace(/\s+/g, '-')
+ .replace(/-+/g, '-')
- const userRoutes = users.map((u: any) => ({
- url: `${baseUrl}/users/${u.username}`,
- lastModified: new Date(), // یا تاریخ آخرین فعالیت کاربر
- priority: 0.6,
- }))
-
- // ۴. واکشی ویدیوها (اگر API جداگانه دارند)
- const videosRes = await fetch('https://api.modstagram.ir/videos/get/all/web', {
- next: { revalidate: 3600 }
- }).catch(() => null)
-
- let videoRoutes: any[] = []
- if (videosRes) {
- const videoData = await videosRes.json()
- const videos = videoData?.videos || []
- videoRoutes = videos.map((v: any) => ({
- url: `${baseUrl}/videos/${v._id}/${encodeURIComponent(v.title.replace(/\s+/g, '-'))}`,
- lastModified: new Date(v.updatedAt || new Date()),
- priority: 0.7,
- }))
+ return {
+ url: `${baseUrl}/billboards/${b._id}/${encodeURIComponent(slug)}`,
+ lastModified: new Date(String(b.updatedAt || new Date())),
+ priority: 0.8,
}
+ })
- // ۵. پستها — از فید عمومی
- let postRoutes: MetadataRoute.Sitemap = []
- try {
- const postsRes = await fetch(
- 'https://app.modstagram.ir/api/v1/users/web?page=1&limit=100',
- { next: { revalidate: 3600 } }
- )
- if (postsRes.ok) {
- const postsData = await postsRes.json()
- const posts = postsData?.posts || []
- postRoutes = posts.map((p: { _id: string; updatedAt?: string }) => ({
- url: `${baseUrl}/posts/${p._id}`,
- lastModified: new Date(p.updatedAt || new Date()),
- changeFrequency: 'weekly' as const,
- priority: 0.65,
- }))
- }
- } catch {
- /* optional */
- }
+ const postsData = await fetchJsonSafe<{ posts?: Array<{ _id: string; updatedAt?: string }> }>(
+ `${API_BASE}/users/web?page=1&limit=100`
+ )
+ const posts = postsData?.posts || []
+ const postRoutes = posts.map((p) => ({
+ url: `${baseUrl}/posts/${p._id}`,
+ lastModified: new Date(p.updatedAt || new Date()),
+ changeFrequency: 'weekly' as const,
+ priority: 0.65,
+ }))
- return [...staticRoutes, ...billboardRoutes, ...userRoutes, ...videoRoutes, ...postRoutes]
- } catch (error) {
- console.error("Sitemap error:", error)
- return staticRoutes
- }
-}
\ No newline at end of file
+ return [...staticRoutes, ...billboardRoutes, ...postRoutes]
+}
diff --git a/src/app/storage/[...path]/route.ts b/src/app/storage/[...path]/route.ts
index ef95f5d..0e00d06 100644
--- a/src/app/storage/[...path]/route.ts
+++ b/src/app/storage/[...path]/route.ts
@@ -4,5 +4,5 @@ type RouteContext = { params: Promise<{ path: string[] }> };
export async function GET(req: Request, context: RouteContext) {
const { path } = await context.params;
- return proxyToUpstream(req, `/storage/${path.join("/")}`);
+ return proxyToUpstream(req, `/storage/${path.join("/")}`, { cacheable: true });
}
diff --git a/src/app/users/[username]/page.tsx b/src/app/users/[username]/page.tsx
index b14e31e..4f5ffb7 100644
--- a/src/app/users/[username]/page.tsx
+++ b/src/app/users/[username]/page.tsx
@@ -1,4 +1,4 @@
-import { BASE_URL, IMAGE_BASE_URL, buildStorageUrl } from "@/components/main/BaseUrl";
+import { buildStorageUrl } from "@/components/main/BaseUrl";
import React from "react";
import { cookies } from "next/headers";
import { User } from "@/types/types";
@@ -7,32 +7,50 @@ import ModelHead from "@/components/models/ModelPage/ModelHead";
import ModelContent from "@/components/models/ModelPage/ModelContent";
import ProfileVisitTracker from "@/components/explore/ProfileVisitTracker";
import { Metadata } from "next";
+import { generatePageMetadata } from "@/utils/generatePageMetadata";
+import { fetchApiJson } from "@/lib/api/fetchApiJson";
interface IUserProps {
params: Promise<{ username: string }>;
}
+type UserWebResponse = { user?: User };
+
+export const dynamic = "force-dynamic";
+
+async function loadUser(username: string, token: string) {
+ const { data } = await fetchApiJson(
+ `/users/get/web?user_name=${encodeURIComponent(username)}`,
+ {
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ }
+ );
+ return data?.user ?? null;
+}
+
export async function generateMetadata({ params }: IUserProps): Promise {
const { username } = await params;
const token = (await cookies()).get("token")?.value || "";
try {
- const res = await fetch(`${BASE_URL}/users/get/web?user_name=${username}`, {
- cache: "no-store",
- headers: { Authorization: token ? `Bearer ${token}` : "" },
- });
- const data = await res.json();
- const user = data?.user as User;
+ const user = await loadUser(username, token);
if (!user) {
- return { title: `${username} | مدستاگرام`, robots: "noindex" };
+ return generatePageMetadata({
+ title: `${username} | مدستاگرام`,
+ description: `پروفایل ${username} در مدستاگرام`,
+ path: `/users/${username}`,
+ type: "profile",
+ });
}
if (user.blocked_you) {
- return {
+ return generatePageMetadata({
title: "پروفایل در دسترس نیست | مدستاگرام",
- robots: "noindex, nofollow",
- };
+ description: "این پروفایل در دسترس نیست.",
+ path: `/users/${username}`,
+ type: "profile",
+ });
}
const fullName = `${user.first_name || ""} ${user.last_name || ""}`.trim();
@@ -40,47 +58,27 @@ export async function generateMetadata({ params }: IUserProps): Promise
diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx
index b2c0759..a848874 100644
--- a/src/components/Layout.tsx
+++ b/src/components/Layout.tsx
@@ -5,6 +5,8 @@ import { ReactQueryProvider } from "@/providers/ReactQueryProvider";
import { Toaster } from "react-hot-toast";
import BackgroundPrefetch from "@/components/BackgroundPrefetch";
import TitleGuardian from "@/components/TitleGuardian";
+import AuthSessionSync from "@/components/auth/AuthSessionSync";
+import PwaInstallPrompt from "@/components/pwa/PwaInstallPrompt";
interface ILayoutProps {
children: React.ReactNode;
@@ -15,6 +17,8 @@ function Layout({ children }: ILayoutProps) {
+
+
{children}
diff --git a/src/components/NewBillboard/Step1.tsx b/src/components/NewBillboard/Step1.tsx
index 499c4df..f52eda0 100644
--- a/src/components/NewBillboard/Step1.tsx
+++ b/src/components/NewBillboard/Step1.tsx
@@ -132,7 +132,7 @@ console.log(categories);
{formik.touched.images && formik.errors.images && (
{formik.errors.images}
)}
-
+
ثبت و ادامه
diff --git a/src/components/NewBillboard/Step2.tsx b/src/components/NewBillboard/Step2.tsx
index 36d8a0b..ef4f78a 100644
--- a/src/components/NewBillboard/Step2.tsx
+++ b/src/components/NewBillboard/Step2.tsx
@@ -62,7 +62,7 @@ const Step2 = ({ nextStep }: { nextStep: () => void }) => {
)} */}
-
+
ثبت و ادامه
void }) => {
typeof formik.errors.selectedFeatures === "string" && (
{formik.errors.selectedFeatures}
)}
-
+
ثبت و ادامه
diff --git a/src/components/NewBillboard/Step4.tsx b/src/components/NewBillboard/Step4.tsx
index c38ec7e..2300ac1 100644
--- a/src/components/NewBillboard/Step4.tsx
+++ b/src/components/NewBillboard/Step4.tsx
@@ -181,7 +181,7 @@ const Step4 = ({ nextStep }: { nextStep: () => void }) => {
-
+
ثبت و ادامه
diff --git a/src/components/NewBillboard/Step5.tsx b/src/components/NewBillboard/Step5.tsx
index 30e79c2..83aa5c4 100644
--- a/src/components/NewBillboard/Step5.tsx
+++ b/src/components/NewBillboard/Step5.tsx
@@ -10,6 +10,7 @@ import useAxios from "@/hooks/useAxios";
import { IAdvertisingType } from "@/types/types";
import { sampleAds, sampleAdsHighlight, sampleAdsSpecial } from "@/constants";
import RoundedDiv from "@/components/elements/RoundedDiv";
+import { selectionCardClass } from "@/lib/ui/buttonStyles";
import { useRouter } from "next/navigation";
import MainBillboardCard from "../MainBillboardCard/MainBillboardCard";
import { dataURLtoBlob } from "@/helpers/helpers";
@@ -247,9 +248,7 @@ const Step5 = () => {
}
/>
{item.name === "normal"
? "نمایش ساده"
@@ -271,7 +270,7 @@ const Step5 = () => {
{formik.touched.selectedType && formik.errors.selectedType && (
{formik.errors.selectedType}
)}
-
+
ثبت درخواست
diff --git a/src/components/TabNavigation.tsx b/src/components/TabNavigation.tsx
index ee72fda..c63fd50 100644
--- a/src/components/TabNavigation.tsx
+++ b/src/components/TabNavigation.tsx
@@ -4,6 +4,7 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import BoldIcon from "@/components/ui/BoldIcon";
import { cn } from "@/lib/utils";
+import { PAGE_SHELL_CLASS } from "@/constants/pageLayout";
const tabs = [
{ href: "/", icon: "home-2", label: "خانه" },
@@ -43,8 +44,13 @@ export default function TabNavigation({ currentPage }: TabNavigationProps) {
if (!mounted) return null;
return (
-