Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
198 changes: 107 additions & 91 deletions apps/frontend/app/(routes)/marketplace/[productSlug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,24 @@
"use client";

import { Share2, ShoppingBag, Star } from "lucide-react";
import {
Clock,
Heart,
MapPin,
MessageSquareMore,
Share2,
ShoppingBag,
Star,
} from "lucide-react";
import { useTranslations } from "next-intl";
import { useEffect, useState } from "react";

import Images from "@/components/products/Images";
import NotFound from "@/components/products/not-found";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { useUtils } from "@/hooks/useUtils";
import { Card, CardContent } from "@/components/ui/card";
import { Separator } from "@/components/ui/separator";
import {
GET_CATEGORIES,
GET_PRODUCTS,
Expand Down Expand Up @@ -36,11 +47,9 @@ interface UIProduct extends FormattedProduct {

const ProductDetails = ({ params }: ProductDetailsProps) => {
const t = useTranslations();
const { renderStars } = useUtils();
const [product, setProduct] = useState<UIProduct | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const [quantity, setQuantity] = useState(1);

// State for data
const [productsData, setProductsData] = useState<ProductsData | undefined>(
Expand Down Expand Up @@ -143,14 +152,6 @@ const ProductDetails = ({ params }: ProductDetailsProps) => {
}
}, [productsData, categoriesData, imagesData, params.productSlug]);

const handleIncreaseQuantity = () => {
setQuantity((prev) => prev + 1);
};

const handleDecreaseQuantity = () => {
setQuantity((prev) => (prev > 1 ? prev - 1 : 1));
};

if (loading) {
return (
<div className="flex justify-center items-center h-64">Loading...</div>
Expand All @@ -170,93 +171,108 @@ const ProductDetails = ({ params }: ProductDetailsProps) => {
}

return (
<div className="flex flex-col md:flex-row justify-center gap-10">
<section className="w-full md:w-1/2">
<div className="grid grid-cols-3 gap-10 pb-3">
<section className="col-span-3 lg:col-span-2">
<Images images={product.images} />
</section>

<section className="w-full md:w-1/2 space-y-6">
<div>
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-zinc-600 text-xl font-semibold mt-3">
${product.price.toFixed(2)}
</p>
</div>

<div>
<h3 className="font-medium mb-2">{t("common.product.category")}:</h3>
<p className="text-gray-700 text-sm leading-relaxed">
{product.category}
</p>
</div>

<div>
<h3 className="font-medium mb-2">{t("common.product.quantity")}:</h3>
<div className="flex items-center gap-2">
<Button
size="icon"
variant="secondary"
aria-label={t("common.product.buttons.decrease")}
onClick={handleDecreaseQuantity}
>
-
</Button>
<span>{quantity}</span>
<Button
size="icon"
variant="secondary"
aria-label={t("common.product.buttons.increase")}
onClick={handleIncreaseQuantity}
>
+
</Button>
</div>
</div>

{product.rating && (
<div className="mt-4 flex items-center">
{renderStars(product.rating).map((star, index) => {
if (star === "filled") {
return <Star key={index} className="h-5 w-5 text-yellow-400" />;
}
if (star === "half") {
return (
<Star
key={index}
className="h-5 w-5 text-yellow-400 opacity-50"
/>
);
}
return <Star key={index} className="h-5 w-5 text-gray-300" />;
})}
<span className="ml-2 text-sm text-gray-600">
{product.rating.toFixed(1)} {t("common.product.reviews")}
</span>
<div className="space-y-6 col-span-3 lg:col-span-1">
<section className="space-y-4">
<div className="flex justify-between">
<Badge>{product.category}</Badge>
<p className="text-muted-foreground">
{t("common.product.posted")}
</p>
</div>
)}

{product.description && (
<div>
<h3 className="font-medium mb-2">
{t("common.product.description")}:
</h3>
<p className="text-gray-700 text-sm leading-relaxed">
{product.description}
<h1 className="text-3xl font-bold">{product.name}</h1>
<p className="text-3xl font-semibold mt-2">
${product.price.toFixed(2)}
</p>
</div>
)}

<div className="flex justify-center items-center gap-3">
<Button className="w-9/12">
<ShoppingBag className="mr-2 h-4 w-4" />
{t("common.product.buttons.buy")}
</Button>
<Button>
<Share2 className="mr-2 h-4 w-4" />
{t("common.product.buttons.share")}
</Button>
</div>
</section>

<Badge variant={"outline"}>{t("common.product.condition")}</Badge>

{product.description && (
<Card>
<CardContent className="p-4">
<h3 className="font-medium mb-2">
{t("common.product.description")}
</h3>
<p className="text-gray-700 text-sm leading-relaxed">
{product.description}
</p>
</CardContent>
</Card>
)}

<div className="flex flex-col justify-center items-center gap-3 w-full">
<Button className="w-full">
<ShoppingBag className="mr-2 h-4 w-4" />
{t("common.product.buttons.buy")}
</Button>
<Button variant={"secondary"} className="w-full">
<MessageSquareMore className="mr-2 h-4 w-4" />
{t("common.productList.chatWithSeller")}
</Button>
<div className="flex justify-center items-center gap-2 w-full">
<Button variant={"outline"} className="w-full">
<Heart />
</Button>
<Button variant={"outline"} className="w-full">
<Share2 />
</Button>
</div>
</div>
</section>

<Separator />

<section>
<Card>
<CardContent className="p-4 space-y-3">
<div className="flex justify-between items-center">
<div className="flex items-center gap-3">
<Avatar>
<AvatarFallback>T</AvatarFallback>
</Avatar>

<div className="flex flex-col">
<h3>TechTreasures</h3>
<div className="flex items-center justify-center gap-1">
<Star
fill="#facc15"
className="h-3 w-3 text-yellow-400"
/>
<p className="text-muted-foreground text-sm">
{t("common.seller.reviews")}
</p>
</div>
</div>
</div>

<p className="text-muted-foreground text-sm">
{t("common.seller.memberSince")}
</p>
</div>

<p className="flex items-center">
<MapPin className="mr-2 h-4 w-4 text-muted-foreground" /> San
Francisco, CA
</p>
<p className="flex items-center">
<Clock className="mr-2 h-4 w-4 text-muted-foreground" />{" "}
{t("common.seller.responseTime")}
</p>

<Button variant={"outline"} className="w-full">
{t("common.seller.viewProfile")}
</Button>
</CardContent>
</Card>
</section>
</div>
</div>
);
};
Expand Down
43 changes: 29 additions & 14 deletions apps/frontend/components/products/Images.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import Image from "next/image";
import {
Carousel,
CarouselContent,
CarouselItem,
CarouselNext,
CarouselPrevious,
} from "../ui/carousel";

interface ImageProps {
images: {
Expand All @@ -12,28 +19,36 @@ const Images = ({ images }: ImageProps) => {
return <p>No images available</p>;
}

const mainImage = images[0];

return (
<div className="flex flex-col gap-8">
<Image
src={mainImage.src}
alt={mainImage.alt}
width={500}
height={450}
className="w-full rounded-lg shadow-lg"
/>
<div className="flex flex-col gap-5">
<Carousel className="w-full">
<CarouselContent>
{images.map((image, index) => (
<CarouselItem key={index}>
<Image
src={image.src}
alt={image.alt}
className="w-full max-h-[600px] rounded-md object-cover"
width={300}
height={400}
/>
</CarouselItem>
))}
</CarouselContent>
<CarouselPrevious className="absolute left-2 top-1/2 -translate-y-1/2 z-10 shadow-md rounded-full" />
<CarouselNext className="absolute right-2 top-1/2 -translate-y-1/2 z-10 shadow-md rounded-full" />
</Carousel>

{images.length > 1 && (
<div className="flex justify-between gap-2">
<div className="flex gap-2">
{images.map((image, index) => (
<Image
width={200}
height={150}
width={100}
height={80}
key={index}
src={image.src}
alt={image.alt}
className="w-1/4 rounded-lg cursor-pointer shadow-lg"
className="w-[100px] h-[80px] rounded-lg cursor-pointer shadow-lg object-cover"
/>
))}
</div>
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/locales/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,8 @@ export const en = {
quantity: "Quantity",
description: "Description",
reviews: "/ 5",
posted: "Posted 2 days ago • 142 views",
condition: "Like New",
buttons: {
buy: "Buy Now",
share: "Share",
Expand All @@ -188,6 +190,10 @@ export const en = {
description:
"Join SafeSwap and reach thousands of potential buyers today!",
btn_label: "Become a Seller",
memberSince: "• Member since January 2022",
reviews: "4.9 (253) reviews",
responseTime: "Usually responds within 2 hours",
viewProfile: "View Seller Profile",
},

createProduct: {
Expand Down
6 changes: 6 additions & 0 deletions apps/frontend/locales/es.ts
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,8 @@ export const es = {
quantity: "Cantidad",
description: "Descripción",
reviews: "/ 5",
posted: "Publicado hace 2 días • 142 vistas",
condition: "Como Nuevo",
buttons: {
buy: "Comprar Ahora",
share: "Compartir",
Expand All @@ -189,6 +191,10 @@ export const es = {
description:
"¡Únete a SafeSwap y llega a miles de compradores hoy mismo!",
btn_label: "Conviértete en Vendedor",
memberSince: "• Miembro desde Eneno 2022",
reviews: "4.9 (253) reseñas",
responseTime: "Usualmente responde en 2 horas",
viewProfile: "Ver Perfil del Vendedor",
},

createProduct: {
Expand Down