diff --git a/apps/web/public/images/Avatar.png b/apps/web/public/images/Avatar.png
new file mode 100644
index 0000000..1761591
Binary files /dev/null and b/apps/web/public/images/Avatar.png differ
diff --git a/apps/web/public/images/product-details/Discount-2.svg b/apps/web/public/images/product-details/Discount-2.svg
new file mode 100644
index 0000000..4652721
--- /dev/null
+++ b/apps/web/public/images/product-details/Discount-2.svg
@@ -0,0 +1,5 @@
+
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}
+
+
+
+
+
+
+
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 (
+
+
+
+
+
+ About the producer
+
+
+
{
+ if (e.key === "Enter" || e.key === " ") {
+ openFarmModal();
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ >
+
+
+ {farmName}
+
+
+
+
+
+
+
+ {[1, 2, 3, 4, 5].map((starIndex) => (
+
+ ))}
+
+
+
+
+
+
+
+
+
+ Sales on Cofiblocks
+
+
+
+ {salesCount}
+
+
+
+
+
+
+
+
+
+ {altitude} metros
+
+
+
+
+
+
+
+
+
+
+ Coordinates
+
+
+
+ {coordinates}
+
+
+
+
{
+ if (e.key === "Enter" || e.key === " ") {
+ onWebsiteClick();
+ }
+ }}
+ role="button"
+ tabIndex={0}
+ >
+
+
+
+
+
+
+
{
+ 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 (
+
+
}
+ showBackButton
+ onBackClick={() => router.back()}
+ hideCart={false}
+ rightActions={
+
+ }
+ />
+
+
+
+
+
+
+
+
+
{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 (
+
+
+
+
+
+ {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}
+
+
+
+
+ ))}
+
+
+ {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 && (