diff --git a/.claude/launch.json b/.claude/launch.json
index 9ddb903..bc5aa17 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -5,7 +5,7 @@
"name": "modstagram-next-dev",
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev"],
- "port": 3000
+ "port": 3004
}
]
}
diff --git a/src/app/(projects)/projects/specialists/page.tsx b/src/app/(projects)/projects/specialists/page.tsx
index 12eacb9..bfe7fdb 100644
--- a/src/app/(projects)/projects/specialists/page.tsx
+++ b/src/app/(projects)/projects/specialists/page.tsx
@@ -97,7 +97,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "280px" : "120px"}
- unoptimized
/>
)
) : (
@@ -144,7 +143,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "64px" : "36px"}
- unoptimized
/>
) : (
diff --git a/src/app/settings/edit/archive/page.tsx b/src/app/settings/edit/archive/page.tsx
index a3ff7ba..9acd654 100644
--- a/src/app/settings/edit/archive/page.tsx
+++ b/src/app/settings/edit/archive/page.tsx
@@ -152,7 +152,6 @@ function ArchivePage() {
fill
className="object-cover"
sizes="50vw"
- unoptimized
/>
)}
diff --git a/src/app/settings/edit/blocked-users/page.tsx b/src/app/settings/edit/blocked-users/page.tsx
index 9f13734..5feeb55 100644
--- a/src/app/settings/edit/blocked-users/page.tsx
+++ b/src/app/settings/edit/blocked-users/page.tsx
@@ -66,8 +66,8 @@ function BlockedUsersPage() {
}
alt=""
fill
+ sizes="44px"
className="object-cover"
- unoptimized
/>
diff --git a/src/app/shops/[shopId]/products/[listingId]/page.tsx b/src/app/shops/[shopId]/products/[listingId]/page.tsx
index 92c5781..9f97aac 100644
--- a/src/app/shops/[shopId]/products/[listingId]/page.tsx
+++ b/src/app/shops/[shopId]/products/[listingId]/page.tsx
@@ -296,7 +296,6 @@ export default function ProductManagePage() {
fill
className="object-cover"
sizes="400px"
- unoptimized
/>
))}
diff --git a/src/app/shops/checkout/[orderId]/page.tsx b/src/app/shops/checkout/[orderId]/page.tsx
index acf3815..22f63bc 100644
--- a/src/app/shops/checkout/[orderId]/page.tsx
+++ b/src/app/shops/checkout/[orderId]/page.tsx
@@ -96,7 +96,6 @@ export default function CheckoutPage() {
fill
className="object-cover"
sizes="64px"
- unoptimized
/>
)}
diff --git a/src/app/shops/listing/[listingId]/ListingDetailClient.tsx b/src/app/shops/listing/[listingId]/ListingDetailClient.tsx
index 40239db..ab54326 100644
--- a/src/app/shops/listing/[listingId]/ListingDetailClient.tsx
+++ b/src/app/shops/listing/[listingId]/ListingDetailClient.tsx
@@ -347,7 +347,6 @@ export default function ListingDetailClient() {
fill
className="object-cover"
sizes="400px"
- unoptimized
/>
))}
diff --git a/src/app/shops/orders/[orderId]/page.tsx b/src/app/shops/orders/[orderId]/page.tsx
index 51bc082..b3ea7d8 100644
--- a/src/app/shops/orders/[orderId]/page.tsx
+++ b/src/app/shops/orders/[orderId]/page.tsx
@@ -356,7 +356,6 @@ export default function OrderDetailPage() {
fill
className="object-cover"
sizes="64px"
- unoptimized
/>
)}
diff --git a/src/app/shops/product/[catalogId]/page.tsx b/src/app/shops/product/[catalogId]/page.tsx
index df08b0e..d851c3f 100644
--- a/src/app/shops/product/[catalogId]/page.tsx
+++ b/src/app/shops/product/[catalogId]/page.tsx
@@ -85,7 +85,6 @@ export default function ProductComparisonPage() {
fill
className="object-cover"
sizes="64px"
- unoptimized
/>
{discountPercent != null && (
)}
diff --git a/src/components/billboards/MainBillboardCard/MainBillboardCard.tsx b/src/components/billboards/MainBillboardCard/MainBillboardCard.tsx
index 7cada18..47a204e 100644
--- a/src/components/billboards/MainBillboardCard/MainBillboardCard.tsx
+++ b/src/components/billboards/MainBillboardCard/MainBillboardCard.tsx
@@ -138,7 +138,6 @@ function MainBillboardCard({
height={120}
alt={billboard?.title}
src={`${IMAGE_BASE_URL}${billboard.images[0]}`}
- unoptimized
/>
) : (
diff --git a/src/components/chat/SharedPostBubble.tsx b/src/components/chat/SharedPostBubble.tsx
index 7b8fc49..86ba5bf 100644
--- a/src/components/chat/SharedPostBubble.tsx
+++ b/src/components/chat/SharedPostBubble.tsx
@@ -52,8 +52,8 @@ export default function SharedPostBubble({ data }: { data: SharedPostPayload })
src={preview}
alt=""
fill
+ sizes="240px"
className="object-cover"
- unoptimized
/>
)}
diff --git a/src/components/chat/SharedProductBubble.tsx b/src/components/chat/SharedProductBubble.tsx
index 87e5133..59ec736 100644
--- a/src/components/chat/SharedProductBubble.tsx
+++ b/src/components/chat/SharedProductBubble.tsx
@@ -38,7 +38,7 @@ export default function SharedProductBubble({ data }: { data: SharedProductPaylo
>
{preview && (
-
+
)}
diff --git a/src/components/chat/SharedStoryBubble.tsx b/src/components/chat/SharedStoryBubble.tsx
index 0ff1368..5642657 100644
--- a/src/components/chat/SharedStoryBubble.tsx
+++ b/src/components/chat/SharedStoryBubble.tsx
@@ -79,8 +79,8 @@ export default function SharedStoryBubble({
src={preview}
alt=""
fill
+ sizes="120px"
className="object-cover"
- unoptimized
/>
)}
diff --git a/src/components/explore/ExploreVideoThumb.tsx b/src/components/explore/ExploreVideoThumb.tsx
index ec2ee81..62bf759 100644
--- a/src/components/explore/ExploreVideoThumb.tsx
+++ b/src/components/explore/ExploreVideoThumb.tsx
@@ -96,7 +96,6 @@ export default function ExploreVideoThumb({
fill
className="object-cover"
sizes="50vw"
- unoptimized
/>
) : !frameReady ? (
diff --git a/src/components/explore/ReelsAcademyCard.tsx b/src/components/explore/ReelsAcademyCard.tsx
index 70ccaa7..9e67115 100644
--- a/src/components/explore/ReelsAcademyCard.tsx
+++ b/src/components/explore/ReelsAcademyCard.tsx
@@ -321,8 +321,8 @@ export default function ReelsAcademyCard({
src={buildStorageUrl(item.profile_image)}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/main/ProfileAvatar.tsx b/src/components/main/ProfileAvatar.tsx
index 324ca61..53c0125 100644
--- a/src/components/main/ProfileAvatar.tsx
+++ b/src/components/main/ProfileAvatar.tsx
@@ -67,9 +67,8 @@ function ProfileAvatar({
= ({ service, onDelete }) => {
height={50}
src={buildStorageUrl(image) || image}
alt={title}
- unoptimized
className="min-w-[50px] min-h-[50px] rounded-lg object-cover"
/>
) : (
diff --git a/src/components/models/AppearanceColorSelect.tsx b/src/components/models/AppearanceColorSelect.tsx
index 69aa74c..ff2e6cf 100644
--- a/src/components/models/AppearanceColorSelect.tsx
+++ b/src/components/models/AppearanceColorSelect.tsx
@@ -43,8 +43,8 @@ export default function AppearanceColorSelect({
src={selected.src}
alt={selected.label}
fill
+ sizes="36px"
className="object-cover"
- unoptimized
/>
) : (
@@ -93,8 +93,8 @@ export default function AppearanceColorSelect({
src={opt.src}
alt={opt.label}
fill
+ sizes="56px"
className="object-cover"
- unoptimized
/>
{opt.label}
diff --git a/src/components/models/MainModelCard/MainModelCard.tsx b/src/components/models/MainModelCard/MainModelCard.tsx
index 3f64c60..f9e620b 100644
--- a/src/components/models/MainModelCard/MainModelCard.tsx
+++ b/src/components/models/MainModelCard/MainModelCard.tsx
@@ -222,9 +222,9 @@ function MainModelCard({
}
alt={user_name}
fill
+ sizes="72px"
className="aspect-square rounded-2xl object-cover"
priority
- unoptimized
/>
diff --git a/src/components/models/MainModelCard/SwipeImageSlider.tsx b/src/components/models/MainModelCard/SwipeImageSlider.tsx
index c5f97fe..799ce17 100644
--- a/src/components/models/MainModelCard/SwipeImageSlider.tsx
+++ b/src/components/models/MainModelCard/SwipeImageSlider.tsx
@@ -120,7 +120,6 @@ const SwipeImageSlider: React.FC = ({
loading={idx === 0 ? "eager" : "lazy"}
className="min-h-[30vh] w-full rounded-2xl object-contain shadow-lg transition-opacity duration-300"
onLoadingComplete={() => setImageLoading?.(false)}
- unoptimized
/>
)}
diff --git a/src/components/models/ModelPage/ModelContentPosts.tsx b/src/components/models/ModelPage/ModelContentPosts.tsx
index 898d322..5faf812 100644
--- a/src/components/models/ModelPage/ModelContentPosts.tsx
+++ b/src/components/models/ModelPage/ModelContentPosts.tsx
@@ -143,7 +143,6 @@ function ModelContentPosts({
height={300}
className="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105 group-active:scale-100"
loading="lazy"
- unoptimized
/>
) : poster ? (
) : (
diff --git a/src/components/posts/PostLinkPicker.tsx b/src/components/posts/PostLinkPicker.tsx
index 4281a72..8fa3804 100644
--- a/src/components/posts/PostLinkPicker.tsx
+++ b/src/components/posts/PostLinkPicker.tsx
@@ -63,8 +63,8 @@ function LinkListItem({
src={imageSrc}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : (
diff --git a/src/components/posts/ReelsImageCarousel.tsx b/src/components/posts/ReelsImageCarousel.tsx
index 1f0bb25..0800a98 100644
--- a/src/components/posts/ReelsImageCarousel.tsx
+++ b/src/components/posts/ReelsImageCarousel.tsx
@@ -153,11 +153,11 @@ export default function ReelsImageCarousel({
src={mediaFiles[0].src}
alt=""
fill
+ sizes="100vw"
className="object-contain select-none"
priority={priority}
draggable={false}
onLoad={() => onLoadingComplete?.()}
- unoptimized
/>
);
@@ -182,6 +182,7 @@ export default function ReelsImageCarousel({
src={file.src}
alt=""
fill
+ sizes="100vw"
className="object-contain select-none"
priority={priority && Math.abs(idx - index) <= 1}
loading={
@@ -191,7 +192,6 @@ export default function ReelsImageCarousel({
onLoad={() => {
if (idx === index) onLoadingComplete?.();
}}
- unoptimized
/>
))}
diff --git a/src/components/posts/ReelsPostActions.tsx b/src/components/posts/ReelsPostActions.tsx
index ea65e30..95b43fc 100644
--- a/src/components/posts/ReelsPostActions.tsx
+++ b/src/components/posts/ReelsPostActions.tsx
@@ -59,8 +59,8 @@ function ReelsProfileFollowButton({
src={profileImage ? buildStorageUrl(profileImage) : "/images/fake-avatar.png"}
alt={userName}
fill
+ sizes="48px"
className="rounded-full object-cover"
- unoptimized
/>
);
diff --git a/src/components/posts/ReelsPostAnalyticsModal.tsx b/src/components/posts/ReelsPostAnalyticsModal.tsx
index b8607e9..236c2f8 100644
--- a/src/components/posts/ReelsPostAnalyticsModal.tsx
+++ b/src/components/posts/ReelsPostAnalyticsModal.tsx
@@ -130,8 +130,8 @@ export default function ReelsPostAnalyticsModal({
src={previewSrc}
alt=""
fill
+ sizes="112px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/posts/ReelsPostCard.tsx b/src/components/posts/ReelsPostCard.tsx
index 19c444c..babeae9 100644
--- a/src/components/posts/ReelsPostCard.tsx
+++ b/src/components/posts/ReelsPostCard.tsx
@@ -437,8 +437,8 @@ export default function ReelsPostCard({
src={videoPoster}
alt=""
fill
+ sizes="100vw"
className="object-contain"
- unoptimized
priority={isActive || preloadPriority === "nearby"}
/>
)}
@@ -611,8 +611,8 @@ export default function ReelsPostCard({
}
alt={user_name}
fill
+ sizes="40px"
className="rounded-full object-cover"
- unoptimized
/>
diff --git a/src/components/posts/SendPostModal.tsx b/src/components/posts/SendPostModal.tsx
index 51d5cf1..4e1ca4b 100644
--- a/src/components/posts/SendPostModal.tsx
+++ b/src/components/posts/SendPostModal.tsx
@@ -137,8 +137,8 @@ export default function SendPostModal({
src={preview}
alt=""
fill
+ sizes="48px"
className="object-cover"
- unoptimized
/>
)}
@@ -181,8 +181,8 @@ export default function SendPostModal({
}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/posts/TagUsersPicker.tsx b/src/components/posts/TagUsersPicker.tsx
index 6ae4b18..a354afe 100644
--- a/src/components/posts/TagUsersPicker.tsx
+++ b/src/components/posts/TagUsersPicker.tsx
@@ -114,8 +114,8 @@ export default function TagUsersPicker({
src={src}
alt=""
fill
+ sizes="28px"
className="object-cover"
- unoptimized
/>
) : null}
@@ -178,8 +178,8 @@ export default function TagUsersPicker({
src={src}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/projects/SpecialistCarousel.tsx b/src/components/projects/SpecialistCarousel.tsx
index d0dd41e..4c687e2 100644
--- a/src/components/projects/SpecialistCarousel.tsx
+++ b/src/components/projects/SpecialistCarousel.tsx
@@ -107,7 +107,6 @@ function SpecialistCard({
loading="lazy"
className="object-cover"
sizes="(min-width: 1024px) 220px, (min-width: 640px) 30vw, 78vw"
- unoptimized
/>
)
) : (
@@ -126,7 +125,6 @@ function SpecialistCard({
loading="lazy"
className="object-cover"
sizes="56px"
- unoptimized
/>
) : (
diff --git a/src/components/projects/SpecialistsSearchModal.tsx b/src/components/projects/SpecialistsSearchModal.tsx
index 8c6fbe9..1a836c0 100644
--- a/src/components/projects/SpecialistsSearchModal.tsx
+++ b/src/components/projects/SpecialistsSearchModal.tsx
@@ -82,7 +82,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "280px" : "120px"}
- unoptimized
/>
)
) : (
@@ -129,7 +128,6 @@ function SpecialistCard({
fill
className="object-cover"
sizes={isMain ? "64px" : "36px"}
- unoptimized
/>
) : (
diff --git a/src/components/settings/ActivityPostGrid.tsx b/src/components/settings/ActivityPostGrid.tsx
index eab7f60..b28eec7 100644
--- a/src/components/settings/ActivityPostGrid.tsx
+++ b/src/components/settings/ActivityPostGrid.tsx
@@ -149,7 +149,6 @@ export default function ActivityPostGrid({ endpoint }: Props) {
fill
className="object-cover"
sizes="50vw"
- unoptimized
/>
)}
diff --git a/src/components/settings/NotificationRow.tsx b/src/components/settings/NotificationRow.tsx
index ed8a8ac..f37cab7 100644
--- a/src/components/settings/NotificationRow.tsx
+++ b/src/components/settings/NotificationRow.tsx
@@ -58,8 +58,8 @@ export default function NotificationRow({
src={avatarSrc}
alt={actor?.user_name || t("common.user")}
fill
+ sizes="48px"
className="object-cover"
- unoptimized
/>
) : (
diff --git a/src/components/settings/VisitorRow.tsx b/src/components/settings/VisitorRow.tsx
index 0ddc6ce..b119361 100644
--- a/src/components/settings/VisitorRow.tsx
+++ b/src/components/settings/VisitorRow.tsx
@@ -40,8 +40,8 @@ export default function VisitorRow({ visitor }: Props) {
src={avatarSrc}
alt={visitor.user_name || t("common.user")}
fill
+ sizes="48px"
className="object-cover"
- unoptimized
/>
diff --git a/src/components/shops/ProductImageLightbox.tsx b/src/components/shops/ProductImageLightbox.tsx
index e2bc624..e1499ad 100644
--- a/src/components/shops/ProductImageLightbox.tsx
+++ b/src/components/shops/ProductImageLightbox.tsx
@@ -71,7 +71,6 @@ export default function ProductImageLightbox({
fill
className="object-contain"
sizes="100vw"
- unoptimized
/>
))}
diff --git a/src/components/shops/SendProductModal.tsx b/src/components/shops/SendProductModal.tsx
index 222dd66..317498e 100644
--- a/src/components/shops/SendProductModal.tsx
+++ b/src/components/shops/SendProductModal.tsx
@@ -125,8 +125,8 @@ export default function SendProductModal({
src={IMAGE_BASE_URL + preview}
alt=""
fill
+ sizes="48px"
className="object-cover"
- unoptimized
/>
)}
@@ -169,8 +169,8 @@ export default function SendProductModal({
}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/shops/ShopProductCommentsModal.tsx b/src/components/shops/ShopProductCommentsModal.tsx
index b155140..4ec1c9b 100644
--- a/src/components/shops/ShopProductCommentsModal.tsx
+++ b/src/components/shops/ShopProductCommentsModal.tsx
@@ -114,8 +114,8 @@ export default function ShopProductCommentsModal({
src={IMAGE_BASE_URL + c.creator.profile_image}
alt=""
fill
+ sizes="36px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/shops/ShopProductListRow.tsx b/src/components/shops/ShopProductListRow.tsx
index d9a79fa..6eb1f58 100644
--- a/src/components/shops/ShopProductListRow.tsx
+++ b/src/components/shops/ShopProductListRow.tsx
@@ -69,7 +69,6 @@ export default function ShopProductListRow({ listing, shopId }: ShopProductListR
fill
className="object-cover"
sizes="80px"
- unoptimized
/>
)}
{discountPercent != null && (
diff --git a/src/components/shops/ShopReelsView.tsx b/src/components/shops/ShopReelsView.tsx
index 4a31a94..b379096 100644
--- a/src/components/shops/ShopReelsView.tsx
+++ b/src/components/shops/ShopReelsView.tsx
@@ -252,7 +252,6 @@ export default function ShopReelsView({
fill
className="object-contain"
sizes="100vw"
- unoptimized
/>
)}
diff --git a/src/components/stories/SendStoryModal.tsx b/src/components/stories/SendStoryModal.tsx
index 6ffa11c..f51e672 100644
--- a/src/components/stories/SendStoryModal.tsx
+++ b/src/components/stories/SendStoryModal.tsx
@@ -144,8 +144,8 @@ export default function SendStoryModal({
src={preview}
alt=""
fill
+ sizes="48px"
className="object-cover"
- unoptimized
/>
)}
@@ -185,8 +185,8 @@ export default function SendStoryModal({
src={buildStorageUrl(u.profile_image)}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>
) : null}
diff --git a/src/components/stories/StoriesBar.tsx b/src/components/stories/StoriesBar.tsx
index 5dff604..34dd5fd 100644
--- a/src/components/stories/StoriesBar.tsx
+++ b/src/components/stories/StoriesBar.tsx
@@ -30,7 +30,6 @@ function StoryAvatar({
width={STORY_AVATAR_SIZE}
height={STORY_AVATAR_SIZE}
className="block h-full w-full object-cover"
- unoptimized
/>
);
}
diff --git a/src/components/stories/StoryMentionPicker.tsx b/src/components/stories/StoryMentionPicker.tsx
index 68f176f..03fda16 100644
--- a/src/components/stories/StoryMentionPicker.tsx
+++ b/src/components/stories/StoryMentionPicker.tsx
@@ -98,7 +98,7 @@ export default function StoryMentionPicker({
>
{src ? (
-
+
) : null}
diff --git a/src/components/stories/StoryViewer.tsx b/src/components/stories/StoryViewer.tsx
index c51058f..8b1c511 100644
--- a/src/components/stories/StoryViewer.tsx
+++ b/src/components/stories/StoryViewer.tsx
@@ -18,6 +18,7 @@ import {
} from "@/api/fetchStories";
import { storyFont, STORY_FONT_WEIGHTS } from "@/lib/storyFonts";
import { buildPostLinkPath, PostLinkKind } from "@/lib/postLinkCaption";
+import { prefetchVideoUrl } from "@/lib/videoPreload";
const LINK_KIND_ICONS: Record
= {
pkg: "teacher",
@@ -175,6 +176,25 @@ export default function StoryViewer({
setMediaAspect(16 / 9);
}, [currentStory?._id]);
+ // پیشبارگذاریِ استوریِ بعدی (همون کاربر یا اولین استوریِ کاربرِ بعدی) — عینِ
+ // prefetchPostVideos توی ریلز که ویدیوی نزدیک رو زودتر گرم میکنه؛ اینجا معادلش برای
+ // سواپِ بعدیِ استوری، چون قبلاً هیچ پیشبارگذاریای برای استوری وجود نداشت
+ useEffect(() => {
+ const nextInSameUser = currentUser?.stories[storyIndex + 1];
+ const nextUser = feed[userIndex + 1];
+ const nextStory = nextInSameUser ?? nextUser?.stories[0];
+ if (!nextStory) return;
+ const nextSrc = nextStory.media_path.startsWith("http")
+ ? nextStory.media_path
+ : buildStorageUrl(nextStory.media_path);
+ if (nextStory.media_type === "video") {
+ prefetchVideoUrl(nextSrc, "low");
+ } else if (typeof window !== "undefined") {
+ const img = new window.Image();
+ img.src = nextSrc;
+ }
+ }, [currentUser, storyIndex, userIndex, feed]);
+
// بوم واقعیِ نمایشدادهشده: contain-fit نسبتِ واقعیِ عکس داخلِ فضای در دسترس — دقیقاً
// همون منطقِ StoryOverlayEditor.tsx (که اینبار نسبتها ازش میان) و mediaBox توی
// اندروید (StoryViewerModal.tsx)
@@ -447,8 +467,8 @@ export default function StoryViewer({
}
alt=""
fill
+ sizes="32px"
className="object-cover"
- unoptimized
/>
@@ -514,9 +534,9 @@ export default function StoryViewer({
src={mediaSrc}
alt=""
fill
+ sizes="(max-width: 1024px) 100vw, 512px"
className="object-contain"
priority
- unoptimized
onLoad={(e) => {
const img = e.currentTarget;
if (img.naturalWidth > 0 && img.naturalHeight > 0) {
@@ -853,8 +873,8 @@ export default function StoryViewer({
}
alt=""
fill
+ sizes="40px"
className="object-cover"
- unoptimized
/>