diff --git a/src/app/(customer)/(shop)/products/[uuid]/page.tsx b/src/app/(customer)/(shop)/products/[uuid]/page.tsx index a9f58d8..1c5927b 100644 --- a/src/app/(customer)/(shop)/products/[uuid]/page.tsx +++ b/src/app/(customer)/(shop)/products/[uuid]/page.tsx @@ -1,7 +1,6 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { winofyFetch, ApiError } from "@/lib/api/winofy"; -import { resolveMediaUrl } from "@/lib/api/media"; import type { ProductDetail } from "@/types/api"; import { AddToCartButton } from "@/components/customer/add-to-cart-button"; @@ -28,9 +27,9 @@ export default async function ProductDetailPage({ params }: PageProps<"/products
- {product.image && ( + {product.image_url && ( // eslint-disable-next-line @next/next/no-img-element - {product.name} + {product.name} )}
diff --git a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx index f3d9930..c0aa9f7 100644 --- a/src/app/(customer)/(shop)/stores/[uuid]/page.tsx +++ b/src/app/(customer)/(shop)/stores/[uuid]/page.tsx @@ -1,7 +1,6 @@ import Link from "next/link"; import { notFound } from "next/navigation"; import { winofyFetch, ApiError, type PaginatedResponse } from "@/lib/api/winofy"; -import { resolveMediaUrl } from "@/lib/api/media"; import type { ProductListItem, StoreDetail } from "@/types/api"; import { AddToCartButton } from "@/components/customer/add-to-cart-button"; @@ -35,17 +34,17 @@ export default async function StoreDetailPage({ params }: PageProps<"/stores/[uu
- {store.cover_image && ( + {store.cover_image_url && ( // eslint-disable-next-line @next/next/no-img-element - + )}
- {store.logo ? ( + {store.logo_url ? ( // eslint-disable-next-line @next/next/no-img-element {store.name} @@ -109,10 +108,10 @@ export default async function StoreDetailPage({ params }: PageProps<"/stores/[uu
- {product.image && ( + {product.image_url && ( // eslint-disable-next-line @next/next/no-img-element {product.name} diff --git a/src/components/customer/cart-item-row.tsx b/src/components/customer/cart-item-row.tsx index aaf8c10..87fb5fd 100644 --- a/src/components/customer/cart-item-row.tsx +++ b/src/components/customer/cart-item-row.tsx @@ -3,7 +3,6 @@ import { useTransition } from "react"; import { useRouter } from "next/navigation"; import { updateCartItemQuantity, removeCartItem } from "@/lib/actions/cart"; -import { resolveClientMediaUrl } from "@/lib/api/media-client"; import type { CartItem } from "@/types/api"; export function CartItemRow({ item }: { item: CartItem }) { @@ -24,9 +23,9 @@ export function CartItemRow({ item }: { item: CartItem }) { return (
- {item.product.image && ( + {item.product.image_url && ( // eslint-disable-next-line @next/next/no-img-element - + )}
diff --git a/src/components/home/site-header.tsx b/src/components/home/site-header.tsx index e837de3..acf7761 100644 --- a/src/components/home/site-header.tsx +++ b/src/components/home/site-header.tsx @@ -14,9 +14,9 @@ export function SiteHeader({ return (
- - {locationLabel ?? "انتخاب محله"} + {locationLabel ?? "انتخاب محله"} + {isAuthenticated ? ( diff --git a/src/components/home/store-card.tsx b/src/components/home/store-card.tsx index eda3c66..bbc42c0 100644 --- a/src/components/home/store-card.tsx +++ b/src/components/home/store-card.tsx @@ -1,5 +1,4 @@ import Link from "next/link"; -import { resolveMediaUrl } from "@/lib/api/media"; import type { StoreListItem } from "@/types/api"; /** winofyfrontenddoc (1).md §3.5 store-card. */ @@ -10,9 +9,9 @@ export function StoreCard({ store }: { store: StoreListItem }) { className="flex items-center gap-3 rounded-xl border border-gray-300 bg-white p-3 shadow-sm" >
- {store.logo && ( + {store.logo_url && ( // eslint-disable-next-line @next/next/no-img-element - {store.name} + {store.name} )}
diff --git a/src/lib/api/media-client.ts b/src/lib/api/media-client.ts deleted file mode 100644 index 3a1361e..0000000 --- a/src/lib/api/media-client.ts +++ /dev/null @@ -1,8 +0,0 @@ -const MEDIA_ORIGIN = process.env.NEXT_PUBLIC_WINOFY_MEDIA_ORIGIN!; - -/** Client-safe counterpart to lib/api/media.ts's resolveMediaUrl. */ -export function resolveClientMediaUrl(path: string | null): string | null { - if (!path) return null; - if (path.startsWith("http://") || path.startsWith("https://")) return path; - return MEDIA_ORIGIN + (path.startsWith("/") ? path : `/${path}`); -} diff --git a/src/lib/api/media.ts b/src/lib/api/media.ts deleted file mode 100644 index 8bcbeff..0000000 --- a/src/lib/api/media.ts +++ /dev/null @@ -1,10 +0,0 @@ -import "server-only"; - -const API_ORIGIN = new URL(process.env.WINOFY_API_BASE_URL!).origin; - -/** Store/product image fields are `null` or an absolute/relative media URL — see FRONTEND_GUIDE.md §4.5. */ -export function resolveMediaUrl(path: string | null): string | null { - if (!path) return null; - if (path.startsWith("http://") || path.startsWith("https://")) return path; - return API_ORIGIN + (path.startsWith("/") ? path : `/${path}`); -} diff --git a/src/types/api.ts b/src/types/api.ts index bc92b32..fc80d4d 100644 --- a/src/types/api.ts +++ b/src/types/api.ts @@ -1,4 +1,12 @@ -/** Shared API types mirrored from FRONTEND_GUIDE.md. Money fields are integer Toman. */ +/** + * Shared API types mirrored from FRONTEND_GUIDE.md. Money fields are integer Toman. + * + * Media fields (`icon`, `image`, `logo`, `cover_image`) are MinIO object keys, not + * URLs — per FRONTEND_GUIDE.md §4.7 (feature/minio-integration), every one of them + * has a `_url` sibling that's a freshly presigned, already-absolute URL. + * Always render the `_url` field directly; never resolve the raw key yourself, and + * don't cache `_url` past the response — it's re-signed (and can change) on every read. + */ export interface City { uuid: string; @@ -17,6 +25,7 @@ export interface StoreCategory { uuid: string; name: string; icon: string | null; + icon_url: string | null; order: number; } @@ -25,7 +34,9 @@ export interface StoreListItem { name: string; category: StoreCategory; logo: string | null; + logo_url: string | null; cover_image: string | null; + cover_image_url: string | null; /** DRF serializes DecimalField as a string, e.g. "3.76" — confirmed against staging. */ rating_avg: string; rating_count: number; @@ -58,6 +69,8 @@ export type UnitType = "gram" | "ml" | "piece"; export interface ProductCategory { uuid: string; name: string; + icon: string | null; + icon_url: string | null; parent: string | null; } @@ -65,6 +78,7 @@ export interface ProductListItem { uuid: string; name: string; image: string | null; + image_url: string | null; price: number; unit_type: UnitType; unit_value: number;