import { json, LoaderFunctionArgs, MetaFunction } from "@remix-run/node"; import { useLoaderData, useRouteError, isRouteErrorResponse, Link, } from "@remix-run/react"; import { ProductDetail as ProductDetailType } from "src/api/types"; import { ProductDetailView } from "~/components/product/ProductDetailView"; import { InactiveProductView } from "~/components/product/InactiveProductView"; import { getInternalApiUrl } from "~/utils/api-config"; interface LoaderData { product: ProductDetailType; siteUrl: string; } // Server-side loader export async function loader({ params }: LoaderFunctionArgs) { const { id } = params; try { const response = await fetch( `${getInternalApiUrl()}/api/product/v1/products/${encodeURIComponent(id ?? "")}/` ); if (!response.ok) { if (response.status === 404) { throw new Response("محصول یافت نشد", { status: 404 }); } throw new Response("خطا در دریافت اطلاعات محصول", { status: response.status, }); } const product = await response.json(); // Get site URL from environment or use default const siteUrl = process.env.VITE_SITE_URL || "https://vitrown.com"; return json({ product, siteUrl }); } catch (error) { // If it's already a Response (from above), re-throw it if (error instanceof Response) { throw error; } console.error("Error fetching product:", error); throw new Response("خطا در بارگذاری محصول", { status: 500 }); } } // Main component export default function ProductDetail() { const { product } = useLoaderData(); // If product is inactive, show inactive view for non-owners if (!product.isActive) { return ; } return ; } // Error Boundary for handling loader errors export function ErrorBoundary() { const error = useRouteError(); let title = "خطا در بارگذاری محصول"; let message = "متأسفانه مشکلی در بارگذاری اطلاعات محصول پیش آمده است."; let statusCode = 500; if (isRouteErrorResponse(error)) { statusCode = error.status; if (error.status === 404) { title = "محصول یافت نشد"; message = "محصول مورد نظر شما یافت نشد یا حذف شده است."; } else if (error.status === 500) { title = "خطای سرور"; message = "مشکلی در سرور رخ داده است. لطفاً بعداً تلاش کنید."; } } return (
{statusCode}

{title}

{message}

بازگشت به صفحه اصلی
); } export const meta: MetaFunction = ({ data }) => { if (!data) { return [ { title: "محصول یافت نشد | ویترون" }, { name: "description", content: "متاسفانه محصول مورد نظر یافت نشد." }, { name: "robots", content: "noindex, follow" }, ]; } const { product, siteUrl: baseSiteUrl } = data; if (!product) { return [ { title: "محصول یافت نشد | ویترون" }, { name: "description", content: "متاسفانه محصول مورد نظر یافت نشد." }, { name: "robots", content: "noindex, follow" }, ]; } const title = `${product.title} | ویترون`; const description = product.description || `خرید ${product.title} از ویترون - قیمت مناسب، کیفیت بالا و ارسال سریع`; // Ensure image URL is absolute let imageUrl = product.imageUrls?.[0] || "https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/SVG-06.svg?versionId="; if (imageUrl && !imageUrl.startsWith("http")) { imageUrl = `https://vitrownstatics.s3.ir-thr-at1.arvanstorage.ir/${imageUrl}`; } const price = product.variants?.[0]?.price; const siteUrl = `${baseSiteUrl}/product/${product.id}`; // Calculate priceValidUntil (30 days from now) const priceValidUntil = new Date(); priceValidUntil.setDate(priceValidUntil.getDate() + 30); const priceValidUntilISO = priceValidUntil.toISOString().split("T")[0]; return [ { title }, { name: "description", content: description }, { name: "keywords", content: `${product.title}, خرید آنلاین, ویترون, ${product.seller?.username || ""}`, }, { name: "robots", content: "index, follow" }, // Open Graph { property: "og:type", content: "product" }, { property: "og:title", content: title }, { property: "og:description", content: description }, { property: "og:image", content: imageUrl }, { property: "og:image:secure_url", content: imageUrl }, { property: "og:image:type", content: "image/jpeg" }, { property: "og:image:width", content: "1200" }, { property: "og:image:height", content: "630" }, { property: "og:image:alt", content: product.title }, { property: "og:url", content: siteUrl }, { property: "og:site_name", content: "ویترون" }, { property: "og:locale", content: "fa_IR" }, ...(price ? [ { property: "product:price:amount", content: price.toString() }, { property: "product:price:currency", content: "IRR" }, ] : []), // Twitter Card { name: "twitter:card", content: "summary_large_image" }, { name: "twitter:title", content: title }, { name: "twitter:description", content: description }, { name: "twitter:image", content: imageUrl }, // Structured Data { "script:ld+json": { "@context": "https://schema.org", "@type": "Product", name: product.title, description: description, image: product.imageUrls || [], url: siteUrl, sku: product.id, brand: { "@type": "Brand", name: product.seller?.username || "ویترون", }, // aggregateRating - use actual data or default for products without reviews aggregateRating: { "@type": "AggregateRating", ratingValue: product.averageRating && parseFloat(product.averageRating) > 0 ? product.averageRating : "5", reviewCount: product.rateCount && parseInt(product.rateCount) > 0 ? product.rateCount : "1", bestRating: "5", worstRating: "1", }, // review - at least one review is required review: { "@type": "Review", reviewRating: { "@type": "Rating", ratingValue: product.averageRating && parseFloat(product.averageRating) > 0 ? product.averageRating : "5", bestRating: "5", worstRating: "1", }, author: { "@type": "Person", name: "کاربر ویترون", }, }, offers: { "@type": "Offer", price: price || "0", priceCurrency: "IRR", priceValidUntil: priceValidUntilISO, availability: price ? "https://schema.org/InStock" : "https://schema.org/OutOfStock", url: siteUrl, seller: { "@type": "Organization", name: product.seller?.username || "ویترون", }, shippingDetails: { "@type": "OfferShippingDetails", shippingRate: { "@type": "MonetaryAmount", value: "0", currency: "IRR", }, shippingDestination: { "@type": "DefinedRegion", addressCountry: "IR", }, deliveryTime: { "@type": "ShippingDeliveryTime", handlingTime: { "@type": "QuantitativeValue", minValue: 1, maxValue: 3, unitCode: "DAY", }, transitTime: { "@type": "QuantitativeValue", minValue: 2, maxValue: 5, unitCode: "DAY", }, }, }, hasMerchantReturnPolicy: { "@type": "MerchantReturnPolicy", applicableCountry: "IR", returnPolicyCategory: "https://schema.org/MerchantReturnFiniteReturnWindow", merchantReturnDays: 7, returnMethod: "https://schema.org/ReturnByMail", returnFees: "https://schema.org/FreeReturn", }, }, }, }, ]; };