From 961b15909c1dc253e40bebe097e9ec87d79c03c8 Mon Sep 17 00:00:00 2001 From: Erick Vasquez <123427413+evgongora@users.noreply.github.com> Date: Mon, 11 Nov 2024 10:49:54 -0600 Subject: [PATCH] feat: product details implementation (#48) --- apps/web/public/images/Avatar.png | Bin 0 -> 9092 bytes .../images/product-details/Discount-2.svg | 5 + .../public/images/product-details/Flame.svg | 5 + .../public/images/product-details/Menu-4.svg | 9 + .../images/product-details/SandClock.svg | 5 + .../images/product-details/Shopping-bag.svg | 5 + .../producer-info/External-link.svg | 8 + .../producer-info/Information-circle.svg | 9 + .../producer-info/Location.svg | 9 + .../product-details/producer-info/Send.svg | 8 + .../producer-info/Star-highlighted.svg | 5 + .../product-details/producer-info/Star.svg | 5 + .../product-details/producer-info/farm.svg | 8 + .../producer-info/shopping-basket.svg | 9 + .../app/_components/features/FarmModal.tsx | 70 +++++ .../app/_components/features/ProducerInfo.tsx | 250 ++++++++++++++++++ .../_components/features/ProductCatalog.tsx | 31 ++- .../_components/features/ProductDetails.tsx | 160 +++++++++++ .../app/_components/features/SearchBar.tsx | 8 +- .../features/SelectionTypeCard.tsx | 83 ++++++ .../web/src/app/_components/features/types.ts | 1 + apps/web/src/app/api/product/[id]/route.ts | 16 ++ apps/web/src/app/product/[id]/page.tsx | 103 ++++++++ apps/web/src/atoms/productAtom.ts | 1 + .../src/server/api/routers/mockProducts.ts | 3 + packages/ui/src/chatWithSeller.tsx | 61 +++++ packages/ui/src/dataCard.tsx | 39 +++ packages/ui/src/infoCard.tsx | 69 +++++ packages/ui/src/pageHeader.tsx | 5 +- 29 files changed, 981 insertions(+), 9 deletions(-) create mode 100644 apps/web/public/images/Avatar.png create mode 100644 apps/web/public/images/product-details/Discount-2.svg create mode 100644 apps/web/public/images/product-details/Flame.svg create mode 100644 apps/web/public/images/product-details/Menu-4.svg create mode 100644 apps/web/public/images/product-details/SandClock.svg create mode 100644 apps/web/public/images/product-details/Shopping-bag.svg create mode 100644 apps/web/public/images/product-details/producer-info/External-link.svg create mode 100644 apps/web/public/images/product-details/producer-info/Information-circle.svg create mode 100644 apps/web/public/images/product-details/producer-info/Location.svg create mode 100644 apps/web/public/images/product-details/producer-info/Send.svg create mode 100644 apps/web/public/images/product-details/producer-info/Star-highlighted.svg create mode 100644 apps/web/public/images/product-details/producer-info/Star.svg create mode 100644 apps/web/public/images/product-details/producer-info/farm.svg create mode 100644 apps/web/public/images/product-details/producer-info/shopping-basket.svg create mode 100644 apps/web/src/app/_components/features/FarmModal.tsx create mode 100644 apps/web/src/app/_components/features/ProducerInfo.tsx create mode 100644 apps/web/src/app/_components/features/ProductDetails.tsx create mode 100644 apps/web/src/app/_components/features/SelectionTypeCard.tsx create mode 100644 apps/web/src/app/api/product/[id]/route.ts create mode 100644 apps/web/src/app/product/[id]/page.tsx create mode 100644 packages/ui/src/chatWithSeller.tsx create mode 100644 packages/ui/src/dataCard.tsx create mode 100644 packages/ui/src/infoCard.tsx diff --git a/apps/web/public/images/Avatar.png b/apps/web/public/images/Avatar.png new file mode 100644 index 0000000000000000000000000000000000000000..17615913a3ef93632aed7caa419fefc7ea17c558 GIT binary patch literal 9092 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 + + + + diff --git a/apps/web/public/images/product-details/Flame.svg b/apps/web/public/images/product-details/Flame.svg new file mode 100644 index 0000000..eedb4ac --- /dev/null +++ b/apps/web/public/images/product-details/Flame.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/images/product-details/Menu-4.svg b/apps/web/public/images/product-details/Menu-4.svg new file mode 100644 index 0000000..7a6dca8 --- /dev/null +++ b/apps/web/public/images/product-details/Menu-4.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/images/product-details/SandClock.svg b/apps/web/public/images/product-details/SandClock.svg new file mode 100644 index 0000000..d6f9050 --- /dev/null +++ b/apps/web/public/images/product-details/SandClock.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/images/product-details/Shopping-bag.svg b/apps/web/public/images/product-details/Shopping-bag.svg new file mode 100644 index 0000000..de17d59 --- /dev/null +++ b/apps/web/public/images/product-details/Shopping-bag.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/images/product-details/producer-info/External-link.svg b/apps/web/public/images/product-details/producer-info/External-link.svg new file mode 100644 index 0000000..1482346 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/External-link.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/web/public/images/product-details/producer-info/Information-circle.svg b/apps/web/public/images/product-details/producer-info/Information-circle.svg new file mode 100644 index 0000000..3160e06 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/Information-circle.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/images/product-details/producer-info/Location.svg b/apps/web/public/images/product-details/producer-info/Location.svg new file mode 100644 index 0000000..bdcc8ea --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/Location.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/public/images/product-details/producer-info/Send.svg b/apps/web/public/images/product-details/producer-info/Send.svg new file mode 100644 index 0000000..a79a001 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/Send.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/web/public/images/product-details/producer-info/Star-highlighted.svg b/apps/web/public/images/product-details/producer-info/Star-highlighted.svg new file mode 100644 index 0000000..e2acec4 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/Star-highlighted.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/images/product-details/producer-info/Star.svg b/apps/web/public/images/product-details/producer-info/Star.svg new file mode 100644 index 0000000..10c4049 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/Star.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/apps/web/public/images/product-details/producer-info/farm.svg b/apps/web/public/images/product-details/producer-info/farm.svg new file mode 100644 index 0000000..8fedeb9 --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/farm.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/apps/web/public/images/product-details/producer-info/shopping-basket.svg b/apps/web/public/images/product-details/producer-info/shopping-basket.svg new file mode 100644 index 0000000..edb43ef --- /dev/null +++ b/apps/web/public/images/product-details/producer-info/shopping-basket.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/web/src/app/_components/features/FarmModal.tsx b/apps/web/src/app/_components/features/FarmModal.tsx new file mode 100644 index 0000000..0a4a35f --- /dev/null +++ b/apps/web/src/app/_components/features/FarmModal.tsx @@ -0,0 +1,70 @@ +import Button from "@repo/ui/button"; +import BottomModal from "~/app/_components/ui/BottomModal"; + +interface FarmModalProps { + isOpen: boolean; + onClose: () => void; + farmData: { + name: string; + since: string; + bio: string; + experiences: string; + goodPractices: string; + }; + isEditable?: boolean; + onEdit: () => void; +} + +function FarmModal({ + isOpen, + onClose, + farmData, + isEditable, + onEdit, +}: FarmModalProps) { + return ( + +
+
+
+
+

+ {farmData.name} +

+

+ producing coffee since {farmData.since} +

+
+ +
+

Bio

+

{farmData.bio}

+
+ +
+

Experiences

+

+ {farmData.experiences} +

+
+ +
+

Good practices

+

+ {farmData.goodPractices} +

+
+ + {isEditable && ( + + )} +
+
+
+
+ ); +} + +export { FarmModal }; diff --git a/apps/web/src/app/_components/features/ProducerInfo.tsx b/apps/web/src/app/_components/features/ProducerInfo.tsx new file mode 100644 index 0000000..b8249b5 --- /dev/null +++ b/apps/web/src/app/_components/features/ProducerInfo.tsx @@ -0,0 +1,250 @@ +import { ChevronRightIcon } from "@heroicons/react/24/outline"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { FarmModal } from "./FarmModal"; + +interface ProducerInfoProps { + farmName: string; + rating: number; + salesCount: number; + altitude: number; + coordinates: string; + onWebsiteClick: () => void; + farmSince?: string; + farmBio?: string; + farmExperiences?: string; + farmGoodPractices?: string; + isEditable?: boolean; +} + +export function ProducerInfo({ + farmName, + rating = 4, + salesCount, + altitude, + coordinates, + onWebsiteClick, + farmSince, + farmBio, + farmExperiences, + farmGoodPractices, + isEditable = false, +}: ProducerInfoProps) { + const [isFarmModalOpen, setIsFarmModalOpen] = useState(false); + const router = useRouter(); + + const farmData = { + name: farmName, + since: farmSince ?? "2020", + bio: farmBio ?? "Farm bio description", + experiences: farmExperiences ?? "Farm experiences", + goodPractices: farmGoodPractices ?? "Farm good practices", + }; + + const openFarmModal = () => setIsFarmModalOpen(true); + const closeFarmModal = () => setIsFarmModalOpen(false); + + return ( +
+
+ Producer Avatar +
+

+ About the producer +

+ +
{ + if (e.key === "Enter" || e.key === " ") { + openFarmModal(); + } + }} + role="button" + tabIndex={0} + > +
+
+ Farm icon +
+ + Farm + +
+
+ {farmName} + +
+
+ +
+
+
+ Reviews icon +
+ + Reviews + +
+
+ {[1, 2, 3, 4, 5].map((starIndex) => ( + {`Star + ))} +
+
+ +
+
+
+ Sales icon +
+ + Sales on Cofiblocks + +
+
+ {salesCount} +
+
+ +
+
+
+ Location icon +
+ + Region + +
+
+ Costa Rica +
+
+ +
+
+
+ Altitude icon +
+ + Altitude + +
+
+ + {altitude} metros + +
+
+ +
+
+
+ Coordinates icon +
+ + Coordinates + +
+
+ {coordinates} +
+
+ +
{ + if (e.key === "Enter" || e.key === " ") { + onWebsiteClick(); + } + }} + role="button" + tabIndex={0} + > +
+
+ Website icon +
+ + Website + +
+
+ +
+
+ + { + closeFarmModal(); + router.push("/user/edit-profile/farm-profile"); + }} + /> +
+ ); +} diff --git a/apps/web/src/app/_components/features/ProductCatalog.tsx b/apps/web/src/app/_components/features/ProductCatalog.tsx index c240609..cc587e5 100755 --- a/apps/web/src/app/_components/features/ProductCatalog.tsx +++ b/apps/web/src/app/_components/features/ProductCatalog.tsx @@ -3,6 +3,7 @@ import { ProductCard } from "@repo/ui/productCard"; import SkeletonLoader from "@repo/ui/skeleton"; import { useAtom } from "jotai"; import Image from "next/image"; +import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import { isLoadingAtom, @@ -19,6 +20,15 @@ export default function ProductCatalog() { const [isLoadingResults, setIsLoading] = useAtom(isLoadingAtom); const [quantity, setQuantityProducts] = useAtom(quantityOfProducts); const [query, setQuery] = useAtom(searchQueryAtom); + const router = useRouter(); + + const utils = api.useUtils(); + + const { mutate: addItem } = api.shoppingCart.addItem.useMutation({ + onSuccess: async () => { + await utils.shoppingCart.getItems.invalidate(); + }, + }); // Using an infinite query to fetch products with pagination const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = @@ -34,7 +44,12 @@ export default function ProductCatalog() { // Effect to update local state whenever new data is loaded useEffect(() => { if (data) { - const allProducts = data.pages.flatMap((page) => page.products); // Flatten the pages to get all products + const allProducts = data.pages.flatMap((page) => + page.products.map((product) => ({ + ...product, + process: product.process ?? "Natural", + })), + ); setProducts(allProducts); } }, [data]); @@ -57,8 +72,8 @@ export default function ProductCatalog() { }, [handleScroll]); // Placeholder for adding products to the cart - const handleAddToCart = (_productId: number) => { - // TODO: Add logic for adding product to cart. + const accessProductDetails = (productId: number) => { + router.push(`/product/${productId}`); // Navigate to the product details page }; // Render each product @@ -85,7 +100,7 @@ export default function ProductCatalog() { price={product.price} badgeText={product.strength} isAddingToShoppingCart={false} // Disable shopping cart action for now - onClick={() => handleAddToCart(product.id)} // Trigger add-to-cart action + onClick={() => accessProductDetails(product.id)} // Trigger add-to-cart action /> ); }; @@ -115,7 +130,9 @@ export default function ProductCatalog() { Clear search - {results.map(renderProduct)} + {results.map((product) => ( +
{renderProduct(product)}
+ ))} ) : query ? (
@@ -138,7 +155,9 @@ export default function ProductCatalog() {
) : ( - products.map(renderProduct) + products.map((product) => ( +
{renderProduct(product)}
+ )) )} {isFetchingNextPage && } diff --git a/apps/web/src/app/_components/features/ProductDetails.tsx b/apps/web/src/app/_components/features/ProductDetails.tsx new file mode 100644 index 0000000..e841cd0 --- /dev/null +++ b/apps/web/src/app/_components/features/ProductDetails.tsx @@ -0,0 +1,160 @@ +import { HeartIcon } from "@heroicons/react/24/outline"; +import { HeartIcon as HeartSolidIcon } from "@heroicons/react/24/solid"; +import Button from "@repo/ui/button"; +import { ChatWithSeller } from "@repo/ui/chatWithSeller"; +import { DataCard } from "@repo/ui/dataCard"; +import PageHeader from "@repo/ui/pageHeader"; +import Image from "next/image"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { ProducerInfo } from "./ProducerInfo"; +import { SelectionTypeCard } from "./SelectionTypeCard"; + +interface ProductDetailsProps { + product: { + image: string; + name: string; + region: string; + farmName: string; + roastLevel: string; + bagsAvailable: number; + price: number; + description: string; + type: "Buyer" | "Farmer" | "SoldOut"; + process: string; + }; +} + +export default function ProductDetails({ product }: ProductDetailsProps) { + const { + image, + name, + region, + farmName, + roastLevel, + bagsAvailable, + price, + type, + description, + process, + } = product; + const [quantity, setQuantity] = useState(1); + const [isLiked, setIsLiked] = useState(false); + const router = useRouter(); + + const isSoldOut = type === "SoldOut"; + const isFarmer = type === "Farmer"; + + return ( +
+
+ {name}
} + showBackButton + onBackClick={() => router.back()} + hideCart={false} + rightActions={ + + } + /> +
+ +
+ {name} +
+ +
+
+

{name}

+

+ {product.description} +

+
+ console.log("Open chat")} + /> +
+
+ + +
+
+ + +
+ + {!isSoldOut && !isFarmer && ( +
+ void 0} + /> +
+ )} + +
+
+
+
+
+ + void 0} + isEditable={true} + /> + + +
+
+
+ ); +} diff --git a/apps/web/src/app/_components/features/SearchBar.tsx b/apps/web/src/app/_components/features/SearchBar.tsx index a9822af..ed13799 100644 --- a/apps/web/src/app/_components/features/SearchBar.tsx +++ b/apps/web/src/app/_components/features/SearchBar.tsx @@ -39,8 +39,12 @@ export default function SearchBar() { setIsLoading(isLoading); if (data?.productsFound) { - setSearchResults(data.productsFound); - setQuantityProducts(data.productsFound.length); + const productsWithProcess = data.productsFound.map((product) => ({ + ...product, + process: product.process ?? "Natural", + })); + setSearchResults(productsWithProcess); + setQuantityProducts(productsWithProcess.length); } else { setSearchResults([]); setQuantityProducts(0); diff --git a/apps/web/src/app/_components/features/SelectionTypeCard.tsx b/apps/web/src/app/_components/features/SelectionTypeCard.tsx new file mode 100644 index 0000000..b2e24bc --- /dev/null +++ b/apps/web/src/app/_components/features/SelectionTypeCard.tsx @@ -0,0 +1,83 @@ +import Button from "@repo/ui/button"; +import { InfoCard } from "@repo/ui/infoCard"; +import { Text } from "@repo/ui/typography"; +import { useState } from "react"; + +interface SelectionTypeCardProps { + price: number; + quantity: number; + bagsAvailable: number; + onQuantityChange: (quantity: number) => void; + onAddToCart: () => void; +} + +export function SelectionTypeCard({ + price, + quantity, + bagsAvailable, + onQuantityChange, + onAddToCart, +}: SelectionTypeCardProps) { + const [selectedOption, setSelectedOption] = useState<"bean" | "grounded">( + "bean", + ); + + const coffeeOptions = [ + { + label: "Bean", + iconSrc: "/images/product-details/Menu-4.svg", + selected: selectedOption === "bean", + onClick: () => setSelectedOption("bean"), + }, + { + label: "Grounded", + iconSrc: "/images/product-details/Menu-4.svg", + selected: selectedOption === "grounded", + onClick: () => setSelectedOption("grounded"), + }, + ]; + + return ( + +
+ + Unit price (340g): {price} USD + +
+ + {price * quantity} USD + + /total +
+
+ +
+ + {quantity} + +
+ + +
+ ); +} diff --git a/apps/web/src/app/_components/features/types.ts b/apps/web/src/app/_components/features/types.ts index 80dc625..dc8755b 100644 --- a/apps/web/src/app/_components/features/types.ts +++ b/apps/web/src/app/_components/features/types.ts @@ -14,6 +14,7 @@ export type Product = { region: string; farmName: string; strength: string; + process?: string; createdAt: Date; updatedAt: Date; }; diff --git a/apps/web/src/app/api/product/[id]/route.ts b/apps/web/src/app/api/product/[id]/route.ts new file mode 100644 index 0000000..9727590 --- /dev/null +++ b/apps/web/src/app/api/product/[id]/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from "next/server"; +import { mockedProducts } from "~/server/api/routers/mockProducts"; + +export async function GET( + request: Request, + { params }: { params: { id: string } }, +) { + const id = Number.parseInt(params.id); + const product = mockedProducts.find((p) => p.id === id); + + if (!product) { + return NextResponse.json({ error: "Product not found" }, { status: 404 }); + } + + return NextResponse.json(product); +} diff --git a/apps/web/src/app/product/[id]/page.tsx b/apps/web/src/app/product/[id]/page.tsx new file mode 100644 index 0000000..12ca56c --- /dev/null +++ b/apps/web/src/app/product/[id]/page.tsx @@ -0,0 +1,103 @@ +"use client"; + +import SkeletonLoader from "@repo/ui/skeleton"; +import { useParams } from "next/navigation"; +import { useEffect, useState } from "react"; +import ProductDetails from "~/app/_components/features/ProductDetails"; + +interface Product { + image: string; + name: string; + region: string; + farmName: string; + roastLevel: string; + bagsAvailable: number; + price: number; + description: string; + type: "Buyer" | "Farmer" | "SoldOut"; + process: string; +} + +interface ApiResponse { + nftMetadata: string; + name: string; + region: string; + farmName: string; + strength: string; + bagsAvailable: number; + price: number; + process?: string; +} + +interface NftMetadata { + imageUrl: string; + description: string; +} + +function ProductPage() { + const params = useParams(); + const productId = typeof params.id === "string" ? params.id : params.id?.[0]; + const [product, setProduct] = useState(null); + const [isLoading, setIsLoading] = useState(true); + + useEffect(() => { + if (productId) { + void fetchProductData(productId); + } + }, [productId]); + + const fetchProductData = async (id: string) => { + try { + setIsLoading(true); + const response = await fetch(`/api/product/${id}`); + if (!response.ok) { + throw new Error(`Error fetching product: ${response.statusText}`); + } + const data = (await response.json()) as ApiResponse; + + const parsedMetadata = JSON.parse(data.nftMetadata) as NftMetadata; + + const product: Product = { + image: parsedMetadata.imageUrl, + name: data.name, + region: data.region, + farmName: data.farmName, + roastLevel: data.strength, + bagsAvailable: data.bagsAvailable ?? 10, + price: data.price, + description: parsedMetadata.description, + type: "SoldOut", + process: data.process ?? "Natural", + }; + + setProduct(product); + } catch (error) { + console.error("Failed to fetch product data:", error); + setProduct(null); + } finally { + setIsLoading(false); + } + }; + + if (isLoading) { + return ( +
+
+ +
+
+ ); + } + + if (!product) { + return ( +
+

Product not found

+
+ ); + } + + return ; +} + +export default ProductPage; diff --git a/apps/web/src/atoms/productAtom.ts b/apps/web/src/atoms/productAtom.ts index c8fcd9d..c65116c 100644 --- a/apps/web/src/atoms/productAtom.ts +++ b/apps/web/src/atoms/productAtom.ts @@ -8,6 +8,7 @@ interface Product { farmName: string; strength: string; nftMetadata: string; + process?: string; createdAt: Date; updatedAt: Date; } diff --git a/apps/web/src/server/api/routers/mockProducts.ts b/apps/web/src/server/api/routers/mockProducts.ts index cf5b9ce..3c1ba02 100644 --- a/apps/web/src/server/api/routers/mockProducts.ts +++ b/apps/web/src/server/api/routers/mockProducts.ts @@ -10,6 +10,7 @@ export const mockedProducts = [ region: "Alajuela", farmName: "Beneficio Las Peñas", strength: "Light", + process: "Honey", nftMetadata: JSON.stringify({ description: "Descripción del Café de Especialidad 1.", imageUrl: "/images/cafe1.webp", @@ -25,6 +26,7 @@ export const mockedProducts = [ region: "Cartago", farmName: "Beneficio Las Nubes", strength: "Medium", + process: "Washed", nftMetadata: JSON.stringify({ description: "Descripción del Café de Especialidad 2.", imageUrl: "/images/cafe2.webp", @@ -40,6 +42,7 @@ export const mockedProducts = [ region: "Heredia", farmName: "Beneficio Monteverde", strength: "Strong", + process: "Natural", nftMetadata: JSON.stringify({ description: "Descripción del Café de Especialidad 3.", imageUrl: "/images/cafe3.webp", diff --git a/packages/ui/src/chatWithSeller.tsx b/packages/ui/src/chatWithSeller.tsx new file mode 100644 index 0000000..2067976 --- /dev/null +++ b/packages/ui/src/chatWithSeller.tsx @@ -0,0 +1,61 @@ +import Image from "next/image"; + +interface ChatWithSellerProps { + name: string; + description: string; + avatarSrc?: string; + onClick?: () => void; +} + +export function ChatWithSeller({ + name, + description, + avatarSrc = "/images/user-profile/avatar.svg", + onClick, +}: ChatWithSellerProps) { + return ( + + ); +} diff --git a/packages/ui/src/dataCard.tsx b/packages/ui/src/dataCard.tsx new file mode 100644 index 0000000..db76124 --- /dev/null +++ b/packages/ui/src/dataCard.tsx @@ -0,0 +1,39 @@ +import Image from "next/image"; + +interface DataCardProps { + label: string; + value: string; + iconSrc?: string; + variant?: "default" | "error"; +} + +export function DataCard({ + label, + value, + iconSrc = "/images/placeholder.svg", + variant = "default", +}: DataCardProps) { + return ( +
+
+ icon +
+
+ {label} + + {value} + +
+
+ ); +} diff --git a/packages/ui/src/infoCard.tsx b/packages/ui/src/infoCard.tsx new file mode 100644 index 0000000..7e8ceaf --- /dev/null +++ b/packages/ui/src/infoCard.tsx @@ -0,0 +1,69 @@ +import Image from "next/image"; +import Button from "./button"; +import { Text } from "./typography"; + +interface Option { + label: string; + iconSrc?: string; + selected?: boolean; + onClick?: () => void; +} + +interface InfoCardProps { + title: string; + options: Option[]; + children?: React.ReactNode; +} + +export function InfoCard({ title, options, children }: InfoCardProps) { + return ( +
+
+ + {title} + + +
+ {options.map((option) => ( +
{ + if (e.key === "Enter" || e.key === " ") { + option.onClick?.(); + } + }} + role="button" + tabIndex={0} + > +
+ {option.label} +
+
+ + {option.label} + +
+
+
+ ))} +
+ + {children} +
+
+ ); +} diff --git a/packages/ui/src/pageHeader.tsx b/packages/ui/src/pageHeader.tsx index b0d31e2..c914cc1 100644 --- a/packages/ui/src/pageHeader.tsx +++ b/packages/ui/src/pageHeader.tsx @@ -10,13 +10,14 @@ const BlockiesSvg = dynamic<{ address: string; size: number; scale: number }>( ); interface PageHeaderProps { - title: string; + title: string | React.ReactNode; userAddress?: string; onLogout?: () => void; hideCart?: boolean; showBackButton?: boolean; onBackClick?: () => void; showBlockie?: boolean; + rightActions?: React.ReactNode; } function PageHeader({ @@ -27,6 +28,7 @@ function PageHeader({ showBackButton = false, onBackClick, showBlockie = true, + rightActions, }: PageHeaderProps) { const [isMenuOpen, setIsMenuOpen] = useState(false); const menuRef = useRef(null); @@ -111,6 +113,7 @@ function PageHeader({

{title}

+ {rightActions} {!hideCart && (