diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..bffb357 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "next/core-web-vitals" +} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d26b105 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js +.yarn/install-state.gz + +# testing +/coverage + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem +.prettierignore + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# local env files +.env*.local +.env + +# vercel +.vercel +.editorconfig + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..a75ac52 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). + +## Getting Started + +First, run the development server: + +```bash +npm run dev +# or +yarn dev +# or +pnpm dev +# or +bun dev +``` + +Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. + +You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. + +[API routes](https://nextjs.org/docs/api-routes/introduction) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. + +The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/api-routes/introduction) instead of React pages. + +This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. + +## Learn More + +To learn more about Next.js, take a look at the following resources: + +- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. +- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. + +You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! + +## Deploy on Vercel + +The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. + +Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. diff --git a/app/components/CheckoutButton.tsx b/app/components/CheckoutButton.tsx new file mode 100644 index 0000000..256ae7c --- /dev/null +++ b/app/components/CheckoutButton.tsx @@ -0,0 +1,39 @@ +import { useState } from "react"; + +const CheckoutButton = () => { + const [loading, setLoading] = useState(false); + + const handleCheckout = async () => { + setLoading(true); + try { + const response = await fetch("/api/checkout", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ + // Your cart details and other necessary data + }), + }); + + const data = await response.json(); + if (data.url) { + window.location.href = data.url; // Redirect to Stripe checkout + } else { + console.error("Checkout session creation failed"); + } + } catch (error) { + console.error("Error during checkout:", error); + } finally { + setLoading(false); + } + }; + + return ( + + ); +}; + +export default CheckoutButton; diff --git a/app/components/Icons.tsx b/app/components/Icons.tsx new file mode 100644 index 0000000..71a41be --- /dev/null +++ b/app/components/Icons.tsx @@ -0,0 +1,39 @@ +import { + LucideProps, + Moon, + SunMedium, + type Icon as LucideIcon, +} from "lucide-react"; + +//export type Icon = typeof LucideIcon + +export const Icons = { + sun: SunMedium, + moon: Moon, + logo: (props: LucideProps) => ( + + + + + ), +}; diff --git a/app/components/ProductData.tsx b/app/components/ProductData.tsx new file mode 100644 index 0000000..b498b1a --- /dev/null +++ b/app/components/ProductData.tsx @@ -0,0 +1,237 @@ +import { client } from "@/sanity/lib/client"; +import React, { ChangeEvent, useEffect, useState, useCallback } from "react"; +import { SanityProduct } from "../config/inventory"; +import { groq } from "next-sanity"; +import Link from "next/link"; +import { shimmer, toBase64 } from "../lib/image"; +import Image from "next/image"; +import { urlForImage } from "@/sanity/lib/image"; +import { formatCurrencyString } from "use-shopping-cart"; + +export default function ProductData() { + const [products, setProducts] = useState([]); + const [filteredProducts, setFilteredProducts] = useState([]); + const [sortedProducts, setSortedProducts] = useState([]); + const [sortType, setSortType] = useState(""); + const [selectedSizes, setSelectedSizes] = useState([]); + + const fetchData = async () => { + try { + const fetchedProducts = await client.fetch( + groq`*[_type == "product"]{ + _id, + "images": images[].asset->url, + name, + "slug": slug.current, + description, + "categories": categories[], + "sizes": sizes[]{ + id, + size, + stripePriceId, + price + }, + colors, + sku, + currency, + _createdAt, + slug + }`, + ); + console.log("Fetched Products:", fetchedProducts); + setProducts(fetchedProducts); + setFilteredProducts(fetchedProducts); + setSortedProducts(fetchedProducts); + } catch (error) { + console.error("Error fetching products:", error); + } + }; + + const sizeProducts = useCallback( + (sizes: string[]) => { + const sizedProducts = products.filter((product) => { + if (product.sizes) { + const filteredSizes = product.sizes.filter((s) => + sizes.includes(s.size), + ); + console.log( + "Filtered Sizes for product:", + product.name, + filteredSizes, + ); + + return filteredSizes.length > 0; + } + return false; + }); + console.log("Sized Products:", sizedProducts); + setFilteredProducts(sizedProducts); + }, + [products], + ); + + const sortProducts = useCallback( + (productsToSort: SanityProduct[], type: string) => { + let sortedProducts = [...productsToSort]; + + if (type === "priceAsc") { + sortedProducts.sort((a, b) => { + const pricesA = a.sizes ? a.sizes.map((size) => size.price) : [0]; + const pricesB = b.sizes ? b.sizes.map((size) => size.price) : [0]; + return Math.min(...pricesA) - Math.min(...pricesB); + }); + } else if (type === "priceDesc") { + sortedProducts.sort((a, b) => { + const pricesA = a.sizes ? a.sizes.map((size) => size.price) : [0]; + const pricesB = b.sizes ? b.sizes.map((size) => size.price) : [0]; + return Math.max(...pricesB) - Math.max(...pricesA); + }); + } else if (type === "newest") { + sortedProducts.sort( + (a, b) => + new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime(), + ); + } + + console.log("Sorted Products:", sortedProducts); + setSortedProducts(sortedProducts); + }, + [], + ); + + useEffect(() => { + fetchData(); + }, []); + + useEffect(() => { + if (selectedSizes.length > 0) { + sizeProducts(selectedSizes); + } else { + setFilteredProducts(products); + } + }, [selectedSizes, sizeProducts, products]); + + useEffect(() => { + sortProducts(filteredProducts, sortType); + }, [sortType, filteredProducts, sortProducts]); + + const handleSortChange = (event: ChangeEvent) => { + setSortType(event.target.value); + }; + + const handleSizeChange = (event: ChangeEvent) => { + const size = event.target.value; + setSelectedSizes((prevSelectedSizes) => + prevSelectedSizes.includes(size) + ? prevSelectedSizes.filter((s) => s !== size) + : [...prevSelectedSizes, size], + ); + }; + + return ( + <> +
+
+

Products to be sold

+ +
+

Sizes:

+
+ + + +
+
+ +
+ + + +
+
+ +
+ {sortedProducts.length > 0 ? ( + sortedProducts.map((product) => ( + +
+ {product.images && product.images[0] && ( + {product.name} + )} +
+

{product.name}

+
+

+ Price:{" "} + {formatCurrencyString({ + currency: product.currency || "EUR", + value: + product.sizes && product.sizes.length > 0 + ? product.sizes[0].price + : 0, + })} +

+
+ + )) + ) : ( +

No products available

+ )} +
+
+ + ); +} diff --git a/app/components/cart-items-empty.tsx b/app/components/cart-items-empty.tsx new file mode 100644 index 0000000..d2053aa --- /dev/null +++ b/app/components/cart-items-empty.tsx @@ -0,0 +1,26 @@ +"use client"; + +import Link from "next/link"; +import { Plus, XCircle } from "lucide-react"; + +import { Button } from "./ui/button"; + +export function CartItemsEmpty() { + return ( +
+
+ +

No products added

+

+ Add products to your cart. +

+ + + +
+
+ ); +} diff --git a/app/components/cart-items.tsx b/app/components/cart-items.tsx new file mode 100644 index 0000000..3fe0bfb --- /dev/null +++ b/app/components/cart-items.tsx @@ -0,0 +1,118 @@ +"use client"; + +import Image from "next/image"; +import Link from "next/link"; +import { urlForImage } from "@/sanity/lib/image"; +import { Clock, X } from "lucide-react"; +import { formatCurrencyString, useShoppingCart } from "use-shopping-cart"; +import { Product } from "use-shopping-cart/core"; + +import { shimmer, toBase64 } from "../lib/image"; +import { getSizeName } from "../lib/utils"; +import { Button } from "./ui/button"; +import { Input } from "./ui/input"; +import { toast, useToast } from "./ui/use-toast"; +import { CartItemsEmpty } from "./cart-items-empty"; +import { product } from "@/sanity/schemas/product-schema"; + +export function CartItems() { + const { cartDetails, removeItem, setItemQuantity } = useShoppingCart(); + const CartItems = Object.entries(cartDetails!).map(([_, product]) => product); + const { toast } = useToast(); + + function removeCartItem(product: Product) { + removeItem(product.id); + toast({ + title: `${product.name} removed`, + description: "Product removed from cart", + variant: "destructive", + }); + } + + if (CartItems.length === 0) return ; + + console.log(cartDetails); + return ( +
    + {CartItems.map((product, productIdx) => ( +
  • +
    + {product.name} +
    +
    +
    +
    +
    +

    + + {product.name} + +

    +
    +

    + {formatCurrencyString({ + value: product.price, + currency: product.currency || "EUR", + })} +

    +

    + Size: {/* @ts-ignore */} + {getSizeName(product.product_data?.size)} +

    +
    +
    + + + setItemQuantity(product.id, Number(event.target.value)) + } + /> +
    + +
    +
    +
    +

    +

    +
    +
  • + ))} +
+ ); +} diff --git a/app/components/cart-summary.tsx b/app/components/cart-summary.tsx new file mode 100644 index 0000000..d8b4f21 --- /dev/null +++ b/app/components/cart-summary.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Loader2 } from "lucide-react"; +import { formatCurrencyString, useShoppingCart } from "use-shopping-cart"; +import { Suspense } from "react"; + +import { Button } from "./ui/button"; + +export function CartSummary() { + const { + formattedTotalPrice, + totalPrice, + cartDetails, + cartCount, + redirectToCheckout, + } = useShoppingCart(); + const [isLoading, setLoading] = useState(false); + const isDisabled = isLoading || cartCount! === 0; + const shippingAmount = cartCount! > 0 ? 500 : 0; + const totalAmount = totalPrice! + shippingAmount; + + async function onCheckout() { + setLoading(true); + /* const cartDetailsEntries = Object.entries( + cartDetails as Record + ); + + const updatedCartDetails = Object.fromEntries( + cartDetailsEntries.map(([id, item]) => [id, item]) + ); */ + + console.log(cartDetails); + + const response = await fetch(`/api/checkout/route`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(cartDetails), + }); + + if (!response.ok) { + const text = await response.text(); + console.error(`Error: ${response.status}, Body: ${text}`); + setLoading(false); + return; + } + /* body: JSON.stringify(updatedCartDetails), + }); + + if (!response.ok) { + const text = await response.text(); + console.error(`Error: ${response.status}, Body: ${text}`); + setLoading(false); + return; + } */ + + const data = await response.json(); + const result = await redirectToCheckout(data.id); + + if (result?.error) { + console.error(result); + } + setLoading(false); + } + + return ( + <> + Loading...}> +
+

+ Order summary +

+
+
+
Subtotal
+ {/* possible issue of hidration {!isLoading ? formattedTotalPrice : 'Loading...'}*/} +
{formattedTotalPrice}
+
+
+
+ Shipping estimate +
+
+ {formatCurrencyString({ + value: shippingAmount, + currency: "EUR", + })} +
+
+
+
Order total
+
+ {formatCurrencyString({ value: totalAmount, currency: "EUR" })} +
+
+
+
+ +
+
+
+ + ); +} diff --git a/app/components/checkout-session.tsx b/app/components/checkout-session.tsx new file mode 100644 index 0000000..a14b5c5 --- /dev/null +++ b/app/components/checkout-session.tsx @@ -0,0 +1,52 @@ +"use client"; + +import { useEffect } from "react"; +import { CheckCheck, XCircle } from "lucide-react"; +import Stripe from "stripe"; +import { useShoppingCart } from "use-shopping-cart"; +import { stripe } from "../lib/stripe"; + +interface Props { + customerDetails: Stripe.Checkout.Session.CustomerDetails | null; +} + +export function CheckoutSession({ customerDetails }: Props) { + const { clearCart } = useShoppingCart(); + + useEffect(() => { + if (customerDetails) { + clearCart(); + } + }, [customerDetails, clearCart]); // ,clearCart]) + + if (!customerDetails) { + return ( + <> + +

+ No checkout session found +

+ + ); + } + + return ( + <> + +

+ Order Successful! +

+

+ Thank you,{" "} + {customerDetails.name}! +

+

+ Check your purchase email{" "} + + {customerDetails.email} + {" "} + for your invoice. +

+ + ); +} diff --git a/app/components/layout.tsx b/app/components/layout.tsx new file mode 100644 index 0000000..6f66c05 --- /dev/null +++ b/app/components/layout.tsx @@ -0,0 +1,18 @@ +import Head from "next/head"; +import { MainNav } from "./main-nav"; +import { siteConfig } from "../config/site"; + +export default function Layout({ children }: { children: React.ReactNode }) { + return ( + <> + + Aufkleber Emporium + + +
+ +
{children}
+
+ + ); +} diff --git a/app/components/main-nav.tsx b/app/components/main-nav.tsx new file mode 100644 index 0000000..f3704b4 --- /dev/null +++ b/app/components/main-nav.tsx @@ -0,0 +1,44 @@ +import Link from "next/link"; +import { siteConfig } from "../config/site"; +import { Icons } from "./Icons"; +import { Button } from "./ui/button"; +import { ShoppingBag } from "lucide-react"; +import { useShoppingCart } from "use-shopping-cart"; + +// logo and site name +export function MainNav() { + const { cartCount } = useShoppingCart(); + + return ( + <> +
+ + {/* */} + +

StickeyLikeMickey🚀

+ + + {siteConfig.name} + + + {siteConfig.footer.map((item) => ( + + ))} + + + +
+ + ); +} diff --git a/app/components/product-filter.tsx b/app/components/product-filter.tsx new file mode 100644 index 0000000..3ff99f9 --- /dev/null +++ b/app/components/product-filter.tsx @@ -0,0 +1,194 @@ +"use client"; + +import { useRouter, useSearchParams } from "next/navigation"; +import { Size } from "../config/inventory"; // Import the 'sizes' type + +import { + Accordion, + AccordionContent, + AccordionItem, + AccordionTrigger, +} from "./ui/accordion"; +import { Checkbox } from "./ui/checkbox"; +import { Key } from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { client } from "@/sanity/lib/client"; +import React from "react"; +import { SanityProduct } from "../config/inventory"; +import router from "next/router"; +import { useFiltering } from "../lib/useFiltering"; + +//import { useFiltering } from "@/lib/useFiltering"; + +type ProductFiltersProps = { + products: SanityProduct[]; + //setFilteredProducts: (products: SanityProduct[]) => void; + + setFilteredProducts: React.Dispatch>; +}; + +const ProductFilters: React.FC = ({ + products, + setFilteredProducts, +}) => { + const [selectedSize, setSelectedSize] = useState(null); + const searchParams = useSearchParams(); + const searchValues: [string, string][] = useMemo(() => { + let values: [string, string][] = []; + if (searchParams) { + values = Array.from(searchParams.entries()); + } + return values; + }, [searchParams]); + + const { + products: filteredProducts, + error, + sizeOptions, + } = useFiltering(searchValues); + + // Update the filtered products + useEffect(() => { + setFilteredProducts(filteredProducts); + }, [filteredProducts, setFilteredProducts]); + + // Handle error state + if (error) { + return
Error: {error.message}
; + } + + /* const newFilteredProducts = products.filter((product) => + searchValues.every(([category, value]) => { + if (category === "category") { + return product.categories.includes(value); + } else if (category === "sizes") { + return product.sizes.map((size) => size.size).includes(value); + } else if (category === "color") { + return product.colors.includes(value); + } else { + return false; // Change this line + } + }) + ); + setFilteredProducts(newFilteredProducts); + //[searchValues, initialProducts, setFilteredProducts] + */ + + /* useEffect(() => { + const fetchData = async () => { + try { + const data = await client.fetch(`*[_type == "product"]`); + setProducts(data); + } catch (error) { + console.error("Failed to fetch products:", error); + // You could also set an error state here to show an error message in your UI + } + }; + + fetchData(); + }, []); + + const sizeSet = useMemo(() => { + return new Set( + products.flatMap((product) => + product.sizes.map((size: Size) => size.size) + ) + ); + }, [products]); + + const sizeOptions = useMemo(() => { + return Array.from(sizeSet).map((size) => ({ + value: size, + label: `⌀ ${String(size)} cm`, + })); + }, [sizeSet]); + */ + + const filters = [ + { + id: "category", + name: "Category", + options: [ + { value: "canvas", label: "Canvas" }, + { value: "stickers", label: "Stickers" }, + ], + }, + { + id: "sizes", + name: "Sizes", + options: sizeOptions, + }, + + { + id: "color", + name: "Color", + options: [ + { value: "black", label: "Black" }, + { value: "blue", label: "Blue" }, + { value: "brown", label: "Brown" }, + { value: "green", label: "Green" }, + { value: "yellow", label: "Yellow" }, + ], + }, + ]; + + return ( +
+

Categories

+ {filters.map((section, i) => ( + + + + + {section.name}{" "} + + {searchParams?.get(section.id) + ? `(${searchParams.get(section.id)})` + : ""} + + + + +
+ {section.options.map((option, optionIdx) => ( +
+ + key === section.id && value === option.value, + )} + onClick={(event) => { + if (searchParams) { + const params = new URLSearchParams(searchParams); // searchParams instead of window.location.search + const checked = + event.currentTarget.dataset.state === "checked"; + checked + ? params.delete(section.id) + : params.set(section.id, String(option.value)); + router.replace(`/?${params.toString()}`); + } + }} + /> + +
+ ))} + {/* */} +
+
+
+
+ ))} +
+ ); +}; + +export default ProductFilters; diff --git a/app/components/product-gallery.tsx b/app/components/product-gallery.tsx new file mode 100644 index 0000000..1ccec5a --- /dev/null +++ b/app/components/product-gallery.tsx @@ -0,0 +1,68 @@ +"use client"; +import { useState } from "react"; +import Image from "next/image"; +import { urlForImage } from "@/sanity/lib/image"; + +import { SanityProduct } from "../config/inventory"; +import { shimmer, toBase64 } from "../lib/image"; + +interface Props { + product: SanityProduct; +} + +export function ProductGallery({ product }: Props) { + const [selectedImage, setSelectedImage] = useState(0); + + return ( +
+ {/* Image Grid */} +
+
    + {product.images.map((image, index) => ( +
    setSelectedImage(index)} + className="relative flex h-24 cursor-pointer items-center justify-center rounded-md bg-white text-sm font-medium uppercase hover:bg-gray-50" + > + + + + {index === selectedImage && ( +
    + ))} +
+
+ + {/* Main Image */} +
+ {`Main +
+
+ ); +} diff --git a/app/components/product-grid.tsx b/app/components/product-grid.tsx new file mode 100644 index 0000000..f08179b --- /dev/null +++ b/app/components/product-grid.tsx @@ -0,0 +1,65 @@ +import React from "react"; +import Link from "next/link"; +import Image from "next/image"; +import { SanityProduct } from "../config/inventory"; +import { shimmer, toBase64 } from "../lib/image"; +import { urlForImage } from "@/sanity/lib/image"; +import { formatCurrencyString } from "use-shopping-cart"; +import { product } from "@/sanity/schemas/product-schema"; + +interface ProductGridProps { + products: SanityProduct[]; +} +//console.log("ProductGridProps:", product); +const ProductGrid: React.FC = ({ products }) => { + // console.log("ProductGrid:", products); + return ( + <> + {/*

+ {products.length} Product{products.length === 1 ? "" : "s"} +

*/} +
+ {products.length > 0 ? ( + products.map((product) => ( + +
+ {product.images && product.images[0] && ( + {product.name} + )} +
+

{product.name}

+
+

+ Price:{" "} + {formatCurrencyString({ + currency: product.currency || "EUR", + value: + product.sizes && product.sizes.length > 0 + ? product.sizes[0].price + : 0, + })} +

+
+ + )) + ) : ( +

No products available

+ )} +
+ + ); +}; + +export default ProductGrid; diff --git a/app/components/product-info.tsx b/app/components/product-info.tsx new file mode 100644 index 0000000..bffe6ab --- /dev/null +++ b/app/components/product-info.tsx @@ -0,0 +1,116 @@ +"use client"; + +import { useState } from "react"; +import Link from "next/link"; +import { ArrowRight } from "lucide-react"; +import { formatCurrencyString, useShoppingCart } from "use-shopping-cart"; + +import { SanityProduct } from "../config/inventory"; +import { getSizeName } from "../lib/utils"; +import { Button } from "./ui/button"; +import { useToast } from "./ui/use-toast"; + +interface Props { + product: SanityProduct; +} + +export function ProductInfo({ product }: Props) { + const [selectedSize, setSelectedSize] = useState(product.sizes[0]); + const [selectedPrice, setSelectedPrice] = useState(product.sizes[0].price); + const { addItem, incrementItem, cartDetails } = useShoppingCart(); + const { toast } = useToast(); + + const isInCart = !!cartDetails?.[`${product.id}-${selectedSize}`]; + + function handleSizeChange(size: SanityProduct["sizes"][0]) { + console.log("handleSizeChange called with size:", size); // This will log when the function is called and what size it's called with + setSelectedSize(size); + setSelectedPrice(size.price); + } + + function addToCart() { + const price = + typeof selectedPrice === "number" + ? selectedPrice + : parseFloat(selectedPrice); + + //console.log(product); // Log a SanityProduct object + //console.log("addToCart", selectedPrice); // Add this line + const itemId = `${product.id}-${selectedSize.size}`; + + const item = { + ...product, + price: price, + id: itemId, + //slug: product.slug, + size: selectedSize.size, + product_data: { + individualItemPrice: price, // Store the individual item price + }, + }; + + if (isInCart) { + incrementItem(itemId); + } else { + addItem(item); + } + toast({ + title: `${product.name} (${getSizeName(selectedSize.size)})`, + description: "Product added to cart", + action: ( + + + + ), + }); + } + + return ( +
+

{product.name}

+ +
+ {/*

Product information

*/} +

+ {formatCurrencyString({ + value: selectedPrice ?? product.sizes[0].price, + currency: product.currency || "EUR", + })} +

+
+
+ {/*

Description

*/} +
{product.description}
+
+
+ {/*

+ Size: Ø {getSizeName(selectedSize.size)} +

*/} + {product.sizes.map((size) => ( + + ))} +
+
+
+ +
+
+
+ ); +} diff --git a/app/components/product-sort.tsx b/app/components/product-sort.tsx new file mode 100644 index 0000000..b70cc0b --- /dev/null +++ b/app/components/product-sort.tsx @@ -0,0 +1,183 @@ +"use client"; + +import { useRouter } from "next/router"; +import { Filter } from "lucide-react"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "./ui/select"; +import { + Sheet, + SheetContent, + SheetDescription, + SheetHeader, + SheetTitle, + SheetTrigger, +} from "./ui/sheet"; +import ProductFilters from "../components/product-filter"; +import { SetStateAction, useEffect, useState } from "react"; +import { SanityProduct } from "../config/inventory"; +import { useSorting } from "@/app/lib/useSorting"; +import { client } from "@/sanity/lib/client"; +import { formatCurrencyString } from "use-shopping-cart"; +import Link from "next/link"; +import { shimmer, toBase64 } from "../lib/image"; +import { urlForImage } from "@/sanity/lib/image"; +import Image from "next/image"; +import { useFiltering } from "@/app/lib/useFiltering"; +import { set } from "sanity"; + +type Props = { + products: SanityProduct[]; + sortedProducts: SanityProduct[]; + //setSortedProducts: React.Dispatch>; +}; + +/* const sortByDateDesc = (a: SanityProduct, b: SanityProduct) => { + return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime(); +}; + +const sortByPriceAsc = (a: SanityProduct, b: SanityProduct) => { + const minPriceA = Math.min(...a.sizes.map((size) => size.price)); + const minPriceB = Math.min(...b.sizes.map((size) => size.price)); + + return minPriceA - minPriceB; +}; +const sortByPriceDesc = (a: SanityProduct, b: SanityProduct) => { + const minPriceA = Math.min(...a.sizes.map((size) => size.price)); + const minPriceB = Math.min(...b.sizes.map((size) => size.price)); + + return minPriceB - minPriceA; +}; */ + +const ProductSort: React.FC = () => { + const router = useRouter(); + /* const [filteredProducts, setFilteredProducts] = useState([]); + const { sortedProducts, setSortOption } = useSorting(products); // Call the useSorting hook + */ + const [products, setProducts] = useState([]); + + const [filteredProducts, setFilteredProducts] = useState([]); + const { sortedProducts, setSortOption } = useSorting(products); + //console.log("sortedProducts:", sortedProducts); + + useEffect(() => { + setProducts(sortedProducts); + // setFilteredProducts(sortedProducts); // Update filteredProducts with the sorted products + }, [sortedProducts]); + + /* +const filterProducts = ( + useFiltering: ( + value: SanityProduct, + index: number, + array: SanityProduct[] + ) => value is SanityProduct + ) => { + const result = products.filter(useFiltering); + setFilteredProducts(result); + console.log("Filtered products:", filterProducts); + }; */ + + // const { products: filteredProducts } = useFiltering(products, searchValues); + //const searchResults = useSearching(filteredProducts, searchTerm); // Replace searchTerm with your actual search term + + //const filteredAndSortedProducts = useFiltering(sortedProducts); // Apply filtering on sortedProducts + const sortOptions = [ + { name: "Newest", value: "/?date=desc" }, + { name: "Price, low to high", value: "/?price=asc" }, + { name: "Price, high to low", value: "/?price=desc" }, + ]; + + /* useEffect(() => { + client.fetch('*[_type == "product"]').then((data: SanityProduct[]) => { + setProducts(data); + }); + }, []); */ + + console.log("Sorted products::::", sortedProducts); + console.log("option:::::", sortOptions); + + return ( + <> +
+ + + + + Categories + + Narrow your product search using the options below. + + + + + + Filters + + +
+ + {/*
+ {sortedProducts.map((product) => ( + +
+ {product.images && product.images[0] && ( + {product.name} + )} +
+

{product.name}

+
+

+ Price:{" "} + {formatCurrencyString({ + currency: product.currency || "EUR", + value: product.sizes[0].price, + })} +

+
+ + ))} +
*/} + + ); +}; + +export default ProductSort; diff --git a/app/components/providers.tsx b/app/components/providers.tsx new file mode 100644 index 0000000..7c393ba --- /dev/null +++ b/app/components/providers.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { CartProvider } from "use-shopping-cart"; + +import { Toaster } from "./ui/toaster"; +/* import { TailwindIndicator } from "./tailwind-indicator"; +import { ThemeProvider } from "./theme-provider"; */ +import { loadStripe, Stripe } from "@stripe/stripe-js"; +import { useEffect, useState } from "react"; + +interface Props { + children: React.ReactNode; +} +const stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!); + +export function Providers({ children }: Props) { + const [stripe, setStripe] = useState(null); + + useEffect(() => { + const initializeStripe = async () => { + const stripeInstance = await loadStripe( + process.env.NEXT_PUBLIC_STRIPE_PUBLIC_KEY!, + ); + setStripe(stripeInstance); + }; + + initializeStripe(); + }, []); + + if (!stripe) { + return null; // or a loading spinner + } + + return ( + + + {children} + {/* + + + */} + + ); +} diff --git a/app/components/submit-btn.tsx b/app/components/submit-btn.tsx new file mode 100644 index 0000000..d01205a --- /dev/null +++ b/app/components/submit-btn.tsx @@ -0,0 +1,27 @@ +import React from "react"; +import { FaPaperPlane } from "react-icons/fa"; + +interface SubmitBtnProps { + isSubmitting: boolean; +} + +const SubmitBtn: React.FC = ({ isSubmitting }) => { + return ( + + ); +}; + +export default SubmitBtn; diff --git a/app/components/ui/accordion.tsx b/app/components/ui/accordion.tsx new file mode 100644 index 0000000..157cf5e --- /dev/null +++ b/app/components/ui/accordion.tsx @@ -0,0 +1,60 @@ +"use client" + +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "../../lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/app/components/ui/button.tsx b/app/components/ui/button.tsx new file mode 100644 index 0000000..4abed27 --- /dev/null +++ b/app/components/ui/button.tsx @@ -0,0 +1,51 @@ +import * as React from "react"; +import { VariantProps, cva } from "class-variance-authority"; + +import { cn } from "../../lib/utils"; + +const buttonVariants = cva( + "inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none ring-offset-background", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-accent hover:text-accent-foreground", + link: "underline-offset-4 hover:underline text-primary", + }, + size: { + default: "h-10 py-2 px-4", + sm: "h-9 px-3 rounded-md", + lg: "h-11 px-8 rounded-md", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +); + +export interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps {} + +const Button = React.forwardRef( + ({ className, variant, size, ...props }, ref) => { + return ( +