From 5ece7572470443e3da3b6b2ade89d0d3d65131cd Mon Sep 17 00:00:00 2001 From: shreyas Date: Sat, 4 Oct 2025 23:55:01 -0400 Subject: [PATCH 1/3] Add merchandise store with product gallery and shopping cart --- docusaurus.config.ts | 5 + src/components/merch/FilterBar.css | 177 +++++++++++++ src/components/merch/FilterBar.tsx | 80 ++++++ src/components/merch/ProductCard.css | 286 ++++++++++++++++++++ src/components/merch/ProductCard.tsx | 107 ++++++++ src/components/merch/ProductGrid.css | 63 +++++ src/components/merch/ProductGrid.tsx | 33 +++ src/components/merch/ShoppingCart.css | 360 ++++++++++++++++++++++++++ src/components/merch/ShoppingCart.tsx | 178 +++++++++++++ src/pages/merch/index.tsx | 267 +++++++++++++++++++ src/pages/merch/merch.css | 238 +++++++++++++++++ static/img/merch/bag.jpeg | Bin 0 -> 3204 bytes static/img/merch/cap.jpeg | Bin 0 -> 3816 bytes static/img/merch/hoodie.jpeg | Bin 0 -> 3710 bytes static/img/merch/hoodie_2.jpeg | Bin 0 -> 3594 bytes static/img/merch/mug.jpeg | Bin 0 -> 2363 bytes static/img/merch/sticker.jpeg | Bin 0 -> 15837 bytes static/img/merch/tshirt.jpeg | Bin 0 -> 4008 bytes static/img/merch/tshirt_2.jpeg | Bin 0 -> 2934 bytes 19 files changed, 1794 insertions(+) create mode 100644 src/components/merch/FilterBar.css create mode 100644 src/components/merch/FilterBar.tsx create mode 100644 src/components/merch/ProductCard.css create mode 100644 src/components/merch/ProductCard.tsx create mode 100644 src/components/merch/ProductGrid.css create mode 100644 src/components/merch/ProductGrid.tsx create mode 100644 src/components/merch/ShoppingCart.css create mode 100644 src/components/merch/ShoppingCart.tsx create mode 100644 src/pages/merch/index.tsx create mode 100644 src/pages/merch/merch.css create mode 100644 static/img/merch/bag.jpeg create mode 100644 static/img/merch/cap.jpeg create mode 100644 static/img/merch/hoodie.jpeg create mode 100644 static/img/merch/hoodie_2.jpeg create mode 100644 static/img/merch/mug.jpeg create mode 100644 static/img/merch/sticker.jpeg create mode 100644 static/img/merch/tshirt.jpeg create mode 100644 static/img/merch/tshirt_2.jpeg diff --git a/docusaurus.config.ts b/docusaurus.config.ts index 76419811..9b90254b 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -156,6 +156,11 @@ const config: Config = { html: '💰 Donate', position: "left", }, + { + to: "/merch", + html: '🛍️ Merch', + position: "left", + }, { type: "dropdown", html: '👩🏻‍💻 Devfolio', diff --git a/src/components/merch/FilterBar.css b/src/components/merch/FilterBar.css new file mode 100644 index 00000000..08867995 --- /dev/null +++ b/src/components/merch/FilterBar.css @@ -0,0 +1,177 @@ +/* Filter Bar Styles */ + +.filter-bar { + background: var(--ifm-card-background-color); + border-bottom: 1px solid var(--ifm-color-emphasis-200); + position: sticky; + top: 60px; + z-index: 100; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.filter-bar-container { + max-width: 1400px; + margin: 0 auto; + padding: 1.5rem 2rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 2rem; + flex-wrap: wrap; +} + +/* Filter Section */ +.filter-section { + flex: 1; + min-width: 300px; +} + +.filter-header { + display: flex; + align-items: center; + gap: 0.5rem; + margin-bottom: 1rem; + color: var(--ifm-font-color-base); +} + +.filter-title { + font-weight: 600; + font-size: 0.875rem; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Category Filters */ +.category-filters { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; +} + +.category-button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.5rem 1rem; + background: var(--ifm-color-emphasis-100); + border: 2px solid transparent; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + color: #2d3748; + cursor: pointer; + transition: all 0.2s ease; +} + +.category-button:hover { + background: var(--ifm-color-emphasis-200); + transform: translateY(-2px); +} + +.category-button.active { + background: var(--ifm-color-primary); + border-color: var(--ifm-color-primary); + color: #ffffff !important; + font-weight: 600; +} + +.category-icon { + font-size: 1.25rem; +} + +.category-label { + white-space: nowrap; +} + +/* Sort Section */ +.sort-section { + min-width: 200px; +} + +.sort-select { + width: 100%; + padding: 0.625rem 1rem; + background: var(--ifm-color-emphasis-100); + border: 2px solid var(--ifm-color-emphasis-200); + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + color: var(--ifm-font-color-base); + cursor: pointer; + transition: all 0.2s ease; + appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + padding-right: 2.5rem; +} + +.sort-select:hover { + border-color: var(--ifm-color-primary); +} + +.sort-select:focus { + outline: none; + border-color: var(--ifm-color-primary); + box-shadow: 0 0 0 3px var(--ifm-color-primary-lightest); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .filter-bar-container { + padding: 1rem; + flex-direction: column; + align-items: stretch; + } + + .filter-section { + min-width: 100%; + } + + .category-filters { + gap: 0.5rem; + } + + .category-button { + padding: 0.5rem 0.75rem; + font-size: 0.8125rem; + } + + .category-icon { + font-size: 1rem; + } + + .sort-section { + min-width: 100%; + } + + .filter-bar { + top: 0; + } +} + +/* Dark Mode */ +[data-theme="dark"] .filter-bar { + background: var(--ifm-background-surface-color); + border-bottom-color: var(--ifm-color-emphasis-300); +} + +[data-theme="dark"] .category-button { + background: var(--ifm-color-emphasis-200); + color: var(--ifm-font-color-base); +} + +[data-theme="dark"] .category-button.active { + background: rgba(16, 185, 129, 0.15); + border-color: var(--ifm-color-primary); + color: #10b981 !important; +} + +[data-theme="dark"] .category-button:hover { + background: var(--ifm-color-emphasis-300); +} + +[data-theme="dark"] .sort-select { + background: var(--ifm-color-emphasis-200); + border-color: var(--ifm-color-emphasis-300); +} diff --git a/src/components/merch/FilterBar.tsx b/src/components/merch/FilterBar.tsx new file mode 100644 index 00000000..a988656d --- /dev/null +++ b/src/components/merch/FilterBar.tsx @@ -0,0 +1,80 @@ +import React from "react"; +import { Filter, SlidersHorizontal } from "lucide-react"; +import "./FilterBar.css"; + +interface FilterBarProps { + selectedCategory: string; + onCategoryChange: (category: string) => void; + sortBy: string; + onSortChange: (sortBy: string) => void; +} + +const categories = [ + { id: "all", label: "All Products", icon: "🛍️" }, + { id: "t-shirts", label: "T-Shirts", icon: "👕" }, + { id: "hoodies", label: "Hoodies", icon: "🧥" }, + { id: "accessories", label: "Accessories", icon: "🎒" }, +]; + +const sortOptions = [ + { value: "featured", label: "Featured" }, + { value: "price-low", label: "Price: Low to High" }, + { value: "price-high", label: "Price: High to Low" }, + { value: "name", label: "Name: A to Z" }, +]; + +const FilterBar: React.FC = ({ + selectedCategory, + onCategoryChange, + sortBy, + onSortChange, +}) => { + return ( +
+
+ {/* Category Filters */} +
+
+ + Categories +
+
+ {categories.map((cat) => ( + + ))} +
+
+ + {/* Sort Options */} +
+
+ + Sort By +
+ +
+
+
+ ); +}; + +export default FilterBar; diff --git a/src/components/merch/ProductCard.css b/src/components/merch/ProductCard.css new file mode 100644 index 00000000..0a171075 --- /dev/null +++ b/src/components/merch/ProductCard.css @@ -0,0 +1,286 @@ +/* Product Card Styles */ + +.product-card { + background: var(--ifm-card-background-color); + border-radius: 16px; + overflow: hidden; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + transition: all 0.3s ease; + height: 100%; + display: flex; + flex-direction: column; +} + +.product-card:hover { + transform: translateY(-8px); + box-shadow: 0 12px 24px rgba(0, 0, 0, 0.15); +} + +/* Image Section */ +.product-card-image-wrapper { + position: relative; + width: 100%; + padding-top: 100%; /* 1:1 Aspect Ratio */ + overflow: hidden; + background: var(--ifm-color-emphasis-100); +} + +.product-card-image { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} + +.product-card-image img { + width: 100%; + height: 100%; + object-fit: cover; + transition: transform 0.3s ease; +} + +.product-card:hover .product-card-image img { + transform: scale(1.05); +} + +.product-card-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 2; +} + +.overlay-button { + background: white; + border: none; + border-radius: 50%; + width: 48px; + height: 48px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: var(--ifm-color-primary); +} + +.overlay-button:hover { + transform: scale(1.1); + background: var(--ifm-color-primary); + color: white; +} + +/* Like Button */ +.product-card-like { + position: absolute; + top: 12px; + right: 12px; + background: rgba(255, 255, 255, 0.9); + border: none; + border-radius: 50%; + width: 36px; + height: 36px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all 0.2s ease; + color: var(--ifm-color-emphasis-600); + z-index: 3; + backdrop-filter: blur(8px); +} + +.product-card-like:hover { + transform: scale(1.1); + background: white; +} + +.product-card-like.liked { + color: var(--ifm-color-danger); +} + +/* Category Badge */ +.product-card-badge { + position: absolute; + top: 12px; + left: 12px; + background: var(--ifm-color-primary); + color: white; + padding: 4px 12px; + border-radius: 20px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + z-index: 3; +} + +/* Content Section */ +.product-card-content { + padding: 1.5rem; + display: flex; + flex-direction: column; + flex-grow: 1; +} + +.product-card-title { + font-size: 1.125rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--ifm-font-color-base); + line-height: 1.4; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; +} + +.product-card-description { + font-size: 0.875rem; + color: var(--ifm-font-color-secondary); + margin-bottom: 1rem; + line-height: 1.5; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + flex-grow: 1; +} + +/* Variants */ +.product-card-variants { + margin-bottom: 1rem; +} + +.variant-group { + display: flex; + align-items: center; + gap: 0.5rem; + flex-wrap: wrap; +} + +.variant-label { + font-size: 0.75rem; + font-weight: 600; + color: var(--ifm-font-color-secondary); + text-transform: uppercase; +} + +.variant-options { + display: flex; + gap: 0.25rem; + flex-wrap: wrap; +} + +.variant-option { + font-size: 0.75rem; + padding: 2px 8px; + background: var(--ifm-color-emphasis-100); + border-radius: 4px; + color: var(--ifm-font-color-base); +} + +/* Footer */ +.product-card-footer { + display: flex; + align-items: center; + justify-content: space-between; + gap: 1rem; + margin-top: auto; + padding-top: 1rem; + border-top: 1px solid var(--ifm-color-emphasis-200); +} + +.product-card-price { + display: flex; + align-items: baseline; + font-weight: 700; + color: var(--ifm-color-primary); +} + +.price-currency { + font-size: 1rem; +} + +.price-amount { + font-size: 1.5rem; +} + +.product-card-button { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.625rem 1.25rem; + background: var(--ifm-color-primary); + color: white; + border: none; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; + white-space: nowrap; +} + +.product-card-button:hover { + background: var(--ifm-color-primary-dark); + transform: translateY(-2px); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); +} + +.product-card-button:active { + transform: translateY(0); +} + +/* Responsive Design */ +@media (max-width: 768px) { + .product-card-content { + padding: 1rem; + } + + .product-card-title { + font-size: 1rem; + } + + .product-card-description { + font-size: 0.8125rem; + } + + .price-amount { + font-size: 1.25rem; + } + + .product-card-button span { + display: none; + } + + .product-card-button { + padding: 0.625rem; + border-radius: 50%; + width: 40px; + height: 40px; + justify-content: center; + } +} + +/* Dark Mode */ +[data-theme="dark"] .product-card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); +} + +[data-theme="dark"] .product-card-like { + background: rgba(0, 0, 0, 0.7); + color: white; +} + +[data-theme="dark"] .overlay-button { + background: var(--ifm-color-emphasis-200); + color: white; +} diff --git a/src/components/merch/ProductCard.tsx b/src/components/merch/ProductCard.tsx new file mode 100644 index 00000000..26eb2d70 --- /dev/null +++ b/src/components/merch/ProductCard.tsx @@ -0,0 +1,107 @@ +import React, { useState } from "react"; +import { motion } from "framer-motion"; +import { ShoppingCart, Heart, Eye } from "lucide-react"; +import type { Product } from "../../pages/merch"; +import "./ProductCard.css"; + +interface ProductCardProps { + product: Product; + onAddToCart: (product: Product) => void; +} + +const ProductCard: React.FC = ({ product, onAddToCart }) => { + const [isLiked, setIsLiked] = useState(false); + const [isHovered, setIsHovered] = useState(false); + + const handleAddToCart = (e: React.MouseEvent) => { + e.preventDefault(); + onAddToCart(product); + }; + + const toggleLike = (e: React.MouseEvent) => { + e.preventDefault(); + setIsLiked(!isLiked); + }; + + return ( + setIsHovered(true)} + onHoverEnd={() => setIsHovered(false)} + > +
+
+ {product.title} + {isHovered && ( + + + + )} +
+ + {product.category && ( + {product.category} + )} +
+ +
+

{product.title}

+

{product.description}

+ + {product.variants && ( +
+ {product.variants.size && ( +
+ Sizes: +
+ {product.variants.size.slice(0, 3).map((size) => ( + + {size} + + ))} + {product.variants.size.length > 3 && ( + +{product.variants.size.length - 3} + )} +
+
+ )} +
+ )} + +
+
+ $ + {product.price.toFixed(2)} +
+ +
+
+
+ ); +}; + +export default ProductCard; diff --git a/src/components/merch/ProductGrid.css b/src/components/merch/ProductGrid.css new file mode 100644 index 00000000..dcf9ea80 --- /dev/null +++ b/src/components/merch/ProductGrid.css @@ -0,0 +1,63 @@ +/* Product Grid Styles */ + +.product-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 2rem; + padding: 2rem 0; +} + +/* Empty State */ +.product-grid-empty { + min-height: 400px; + display: flex; + align-items: center; + justify-content: center; + padding: 4rem 2rem; +} + +.empty-state { + text-align: center; + max-width: 400px; +} + +.empty-icon { + font-size: 4rem; + display: block; + margin-bottom: 1rem; +} + +.empty-state h3 { + font-size: 1.5rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--ifm-font-color-base); +} + +.empty-state p { + font-size: 1rem; + color: var(--ifm-font-color-secondary); + margin: 0; +} + +/* Responsive Design */ +@media (max-width: 1200px) { + .product-grid { + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 1.5rem; + } +} + +@media (max-width: 768px) { + .product-grid { + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 1rem; + } +} + +@media (max-width: 480px) { + .product-grid { + grid-template-columns: 1fr; + gap: 1rem; + } +} diff --git a/src/components/merch/ProductGrid.tsx b/src/components/merch/ProductGrid.tsx new file mode 100644 index 00000000..037a2fab --- /dev/null +++ b/src/components/merch/ProductGrid.tsx @@ -0,0 +1,33 @@ +import React from "react"; +import ProductCard from "./ProductCard"; +import type { Product } from "../../pages/merch"; +import "./ProductGrid.css"; + +interface ProductGridProps { + products: Product[]; + onAddToCart: (product: Product) => void; +} + +const ProductGrid: React.FC = ({ products, onAddToCart }) => { + if (products.length === 0) { + return ( +
+
+ 🔍 +

No products found

+

Try adjusting your filters or check back later for new items!

+
+
+ ); + } + + return ( +
+ {products.map((product) => ( + + ))} +
+ ); +}; + +export default ProductGrid; diff --git a/src/components/merch/ShoppingCart.css b/src/components/merch/ShoppingCart.css new file mode 100644 index 00000000..7653d509 --- /dev/null +++ b/src/components/merch/ShoppingCart.css @@ -0,0 +1,360 @@ +/* Shopping Cart Styles */ + +.cart-backdrop { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.5); + z-index: 1000; + backdrop-filter: blur(4px); +} + +.cart-panel { + position: fixed; + top: 0; + right: 0; + bottom: 0; + width: 100%; + max-width: 450px; + background: var(--ifm-background-color); + box-shadow: -4px 0 24px rgba(0, 0, 0, 0.2); + z-index: 1001; + display: flex; + flex-direction: column; +} + +/* Header */ +.cart-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 1.5rem; + border-bottom: 1px solid var(--ifm-color-emphasis-200); +} + +.cart-header-title { + display: flex; + align-items: center; + gap: 0.75rem; + color: var(--ifm-font-color-base); +} + +.cart-header-title h2 { + font-size: 1.25rem; + font-weight: 700; + margin: 0; +} + +.cart-count { + font-size: 0.875rem; + color: var(--ifm-font-color-secondary); +} + +.cart-close-button { + background: none; + border: none; + color: var(--ifm-font-color-base); + cursor: pointer; + padding: 0.5rem; + border-radius: 50%; + transition: all 0.2s ease; + display: flex; + align-items: center; + justify-content: center; +} + +.cart-close-button:hover { + background: var(--ifm-color-emphasis-100); +} + +/* Content */ +.cart-content { + flex: 1; + overflow-y: auto; + display: flex; + flex-direction: column; +} + +/* Empty State */ +.cart-empty { + flex: 1; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 3rem 2rem; + text-align: center; +} + +.empty-icon { + color: var(--ifm-color-emphasis-400); + margin-bottom: 1rem; +} + +.cart-empty h3 { + font-size: 1.25rem; + font-weight: 700; + margin-bottom: 0.5rem; + color: var(--ifm-font-color-base); +} + +.cart-empty p { + font-size: 0.875rem; + color: var(--ifm-font-color-secondary); + margin-bottom: 1.5rem; +} + +.cart-continue-button { + padding: 0.75rem 1.5rem; + background: var(--ifm-color-primary); + color: white; + border: none; + border-radius: 8px; + font-weight: 600; + cursor: pointer; + transition: all 0.2s ease; +} + +.cart-continue-button:hover { + background: var(--ifm-color-primary-dark); + transform: translateY(-2px); +} + +/* Cart Items */ +.cart-items { + flex: 1; + padding: 1rem; + display: flex; + flex-direction: column; + gap: 1rem; +} + +.cart-item { + display: flex; + gap: 1rem; + padding: 1rem; + background: var(--ifm-card-background-color); + border-radius: 12px; + border: 1px solid var(--ifm-color-emphasis-200); +} + +.cart-item-image { + width: 80px; + height: 80px; + border-radius: 8px; + overflow: hidden; + flex-shrink: 0; + background: var(--ifm-color-emphasis-100); +} + +.cart-item-image img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.cart-item-details { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.cart-item-title { + font-size: 0.9375rem; + font-weight: 600; + margin: 0; + color: var(--ifm-font-color-base); + line-height: 1.3; +} + +.cart-item-price { + font-size: 0.875rem; + font-weight: 700; + color: var(--ifm-color-primary); + margin: 0; +} + +.cart-item-actions { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: auto; +} + +/* Quantity Controls */ +.quantity-controls { + display: flex; + align-items: center; + gap: 0.5rem; + background: var(--ifm-color-emphasis-100); + border-radius: 6px; + padding: 0.25rem; +} + +.quantity-controls button { + width: 28px; + height: 28px; + border: none; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.quantity-controls button:hover:not(:disabled) { + background: var(--ifm-color-primary); + color: white; +} + +.quantity-controls button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.quantity-display { + min-width: 32px; + text-align: center; + font-weight: 600; + font-size: 0.875rem; + color: var(--ifm-font-color-base); +} + +.cart-item-remove { + width: 28px; + height: 28px; + border: none; + background: var(--ifm-color-danger-lightest); + color: var(--ifm-color-danger); + border-radius: 4px; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; +} + +.cart-item-remove:hover { + background: var(--ifm-color-danger); + color: white; +} + +/* Footer */ +.cart-footer { + border-top: 1px solid var(--ifm-color-emphasis-200); + padding: 1.5rem; + background: var(--ifm-background-surface-color); +} + +.cart-notice { + padding: 0.75rem 1rem; + background: var(--ifm-color-warning-lightest); + border-left: 3px solid var(--ifm-color-warning); + border-radius: 6px; + margin-bottom: 1rem; + display: flex; + align-items: center; + gap: 0.75rem; +} + +.cart-notice.success { + background: var(--ifm-color-success-lightest); + border-left-color: var(--ifm-color-success); +} + +.cart-notice span { + font-size: 1.25rem; +} + +.cart-notice p { + margin: 0; + font-size: 0.875rem; + color: var(--ifm-font-color-base); + font-weight: 500; +} + +/* Totals */ +.cart-totals { + margin-bottom: 1.5rem; +} + +.cart-total-row { + display: flex; + justify-content: space-between; + padding: 0.5rem 0; + font-size: 0.9375rem; + color: var(--ifm-font-color-base); +} + +.cart-total-row.total { + padding-top: 1rem; + margin-top: 0.5rem; + border-top: 2px solid var(--ifm-color-emphasis-200); + font-size: 1.125rem; + font-weight: 700; + color: var(--ifm-color-primary); +} + +/* Checkout Button */ +.cart-checkout-button { + width: 100%; + padding: 1rem; + background: var(--ifm-color-primary); + color: white; + border: none; + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + transition: all 0.2s ease; + margin-bottom: 0.75rem; +} + +.cart-checkout-button:hover { + background: var(--ifm-color-primary-dark); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2); +} + +.cart-secure-text { + text-align: center; + font-size: 0.75rem; + color: var(--ifm-font-color-secondary); + margin: 0; +} + +/* Responsive */ +@media (max-width: 480px) { + .cart-panel { + max-width: 100%; + } + + .cart-item-image { + width: 64px; + height: 64px; + } + + .cart-item-title { + font-size: 0.875rem; + } +} + +/* Dark Mode */ +[data-theme="dark"] .cart-panel { + background: var(--ifm-background-surface-color); +} + +[data-theme="dark"] .cart-item { + background: var(--ifm-background-color); + border-color: var(--ifm-color-emphasis-300); +} diff --git a/src/components/merch/ShoppingCart.tsx b/src/components/merch/ShoppingCart.tsx new file mode 100644 index 00000000..e3abe3cd --- /dev/null +++ b/src/components/merch/ShoppingCart.tsx @@ -0,0 +1,178 @@ +import React from "react"; +import { X, Plus, Minus, ShoppingBag, ArrowRight } from "lucide-react"; +import { motion, AnimatePresence } from "framer-motion"; +import type { Product } from "../../pages/merch"; +import "./ShoppingCart.css"; + +interface ShoppingCartProps { + isOpen: boolean; + onClose: () => void; + items: Array; + onUpdateQuantity: (productId: string, quantity: number) => void; + onRemoveItem: (productId: string) => void; +} + +const ShoppingCart: React.FC = ({ + isOpen, + onClose, + items, + onUpdateQuantity, + onRemoveItem, +}) => { + const subtotal = items.reduce((sum, item) => sum + item.price * item.quantity, 0); + const shipping = subtotal > 50 ? 0 : 5.99; + const total = subtotal + shipping; + + const handleCheckout = () => { + // TODO: Integrate with Shopify checkout + alert( + "Shopify checkout integration coming soon! This will redirect to Shopify's secure checkout." + ); + }; + + return ( + + {isOpen && ( + <> + {/* Backdrop */} + + + {/* Cart Panel */} + + {/* Header */} +
+
+ +

Shopping Cart

+ ({items.length}) +
+ +
+ + {/* Content */} +
+ {items.length === 0 ? ( +
+ +

Your cart is empty

+

Add some awesome Recode merch to get started!

+ +
+ ) : ( + <> + {/* Cart Items */} +
+ {items.map((item) => ( + +
+ {item.title} +
+
+

{item.title}

+

${item.price.toFixed(2)}

+
+
+ + {item.quantity} + +
+ +
+
+
+ ))} +
+ + {/* Footer */} +
+ {/* Shipping Notice */} + {subtotal < 50 && ( +
+ 💡 +

Add ${(50 - subtotal).toFixed(2)} more for free shipping!

+
+ )} + {subtotal >= 50 && ( +
+ +

You qualify for free shipping!

+
+ )} + + {/* Totals */} +
+
+ Subtotal: + ${subtotal.toFixed(2)} +
+
+ Shipping: + {shipping === 0 ? "FREE" : `$${shipping.toFixed(2)}`} +
+
+ Total: + ${total.toFixed(2)} +
+
+ + {/* Checkout Button */} + + +

+ 🔒 Secure checkout powered by Shopify +

+
+ + )} +
+
+ + )} +
+ ); +}; + +export default ShoppingCart; diff --git a/src/pages/merch/index.tsx b/src/pages/merch/index.tsx new file mode 100644 index 00000000..2ad4c0ad --- /dev/null +++ b/src/pages/merch/index.tsx @@ -0,0 +1,267 @@ +import React, { useState, useEffect } from "react"; +import type { ReactNode } from "react"; +import Layout from "@theme/Layout"; +import { motion } from "framer-motion"; +import ProductGrid from "../../components/merch/ProductGrid"; +import FilterBar from "../../components/merch/FilterBar"; +import ShoppingCart from "../../components/merch/ShoppingCart"; +import { ShoppingBag } from "lucide-react"; +import "./merch.css"; + +export interface Product { + id: string; + title: string; + description: string; + price: number; + image: string; + category: string; + variants?: { + size?: string[]; + color?: string[]; + }; + shopifyId?: string; +} + +// Sample products - Replace with actual Shopify data +const sampleProducts: Product[] = [ + { + id: "1", + title: "Recode Hive Classic T-Shirt", + description: "Premium cotton t-shirt with Recode Hive logo", + price: 29.99, + image: "/img/merch/tshirt.jpeg", + category: "t-shirts", + variants: { + size: ["S", "M", "L", "XL", "XXL"], + color: ["Black", "White", "Navy"], + }, + }, + { + id: "2", + title: "Code & Coffee Hoodie", + description: "Cozy hoodie perfect for coding sessions", + price: 49.99, + image: "/img/merch/hoodie.jpeg", + category: "hoodies", + variants: { + size: ["S", "M", "L", "XL", "XXL"], + color: ["Navy", "Gray", "Black"], + }, + }, + { + id: "3", + title: "Recode Laptop Sticker Pack", + description: "Set of 5 waterproof vinyl stickers", + price: 9.99, + image: "/img/merch/sticker.jpeg", + category: "accessories", + }, + { + id: "4", + title: "Developer's Mug", + description: "Ceramic mug with inspiring code quotes", + price: 14.99, + image: "/img/merch/mug.jpeg", + category: "accessories", + }, + { + id: "5", + title: "Recode Tote Bag", + description: "Durable canvas tote for your laptop and books", + price: 19.99, + image: "/img/merch/bag.jpeg", + category: "accessories", + }, + { + id: "6", + title: "Recode Cap", + description: "Adjustable snapback cap with embroidered logo", + price: 24.99, + image: "/img/merch/cap.jpeg", + category: "accessories", + }, +]; + +export default function MerchPage(): ReactNode { + const [selectedCategory, setSelectedCategory] = useState("all"); + const [sortBy, setSortBy] = useState("featured"); + const [filteredProducts, setFilteredProducts] = useState(sampleProducts); + const [cartOpen, setCartOpen] = useState(false); + const [cartItems, setCartItems] = useState>([]); + + useEffect(() => { + let filtered = [...sampleProducts]; + + // Filter by category + if (selectedCategory !== "all") { + filtered = filtered.filter((p) => p.category === selectedCategory); + } + + // Sort products + switch (sortBy) { + case "price-low": + filtered.sort((a, b) => a.price - b.price); + break; + case "price-high": + filtered.sort((a, b) => b.price - a.price); + break; + case "name": + filtered.sort((a, b) => a.title.localeCompare(b.title)); + break; + default: + // featured - keep original order + break; + } + + setFilteredProducts(filtered); + }, [selectedCategory, sortBy]); + + const addToCart = (product: Product) => { + setCartItems((prev) => { + const existing = prev.find((item) => item.id === product.id); + if (existing) { + return prev.map((item) => + item.id === product.id ? { ...item, quantity: item.quantity + 1 } : item + ); + } + return [...prev, { ...product, quantity: 1 }]; + }); + setCartOpen(true); + }; + + const removeFromCart = (productId: string) => { + setCartItems((prev) => prev.filter((item) => item.id !== productId)); + }; + + const updateQuantity = (productId: string, quantity: number) => { + if (quantity === 0) { + removeFromCart(productId); + return; + } + setCartItems((prev) => + prev.map((item) => (item.id === productId ? { ...item, quantity } : item)) + ); + }; + + const cartItemCount = cartItems.reduce((sum, item) => sum + item.quantity, 0); + + return ( + +
+ {/* Hero Section */} +
+ +

+ + Official Recode Merch +

+

+ Wear your code pride! Premium quality apparel and accessories for developers + who love open source. +

+
+
+ 100% + Quality +
+
+ 🌍 + Worldwide Shipping +
+
+ 💚 + Eco-Friendly +
+
+
+
+ + {/* Filter Bar */} + + + {/* Products Grid */} +
+ +
+ + {/* Shopping Cart */} + setCartOpen(false)} + items={cartItems} + onUpdateQuantity={updateQuantity} + onRemoveItem={removeFromCart} + /> + + {/* Floating Cart Button */} + + + {/* Info Section */} +
+
+ +

🚚 Free Shipping

+

Free shipping on orders over $50

+
+ +

🔄 Easy Returns

+

30-day return policy, no questions asked

+
+ +

🌱 Sustainable

+

Eco-friendly materials and ethical production

+
+ +

💯 Quality Guarantee

+

Premium materials, built to last

+
+
+
+
+
+ ); +} diff --git a/src/pages/merch/merch.css b/src/pages/merch/merch.css new file mode 100644 index 00000000..9df34a5e --- /dev/null +++ b/src/pages/merch/merch.css @@ -0,0 +1,238 @@ +/* Merch Page Styles */ + +.merch-page { + min-height: 100vh; + background: var(--ifm-background-color); + color: var(--ifm-font-color-base); + padding-bottom: 4rem; +} + +/* Hero Section */ +.merch-hero { + padding: 4rem 2rem; + text-align: center; + background: linear-gradient( + 135deg, + #16a34a 0%, + #15803d 50%, + #166534 100% + ) !important; + margin-bottom: 3rem; + position: relative; + overflow: hidden; +} + +.merch-hero::before { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: url("data:image/svg+xml,%3Csvg width='60' height='60' viewBox='0 0 60 60' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.05'%3E%3Cpath d='M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E"); + opacity: 0.3; +} + +.merch-hero-content { + position: relative; + z-index: 1; + max-width: 900px; + margin: 0 auto; +} + +.merch-hero .merch-hero-content .merch-hero-title { + font-size: 3rem; + font-weight: 800; + margin-bottom: 1rem; + line-height: 1.2; + color: #1a1a1a !important; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3) !important; + background: none !important; + -webkit-text-fill-color: #1a1a1a !important; + background-clip: unset !important; + -webkit-background-clip: unset !important; +} + +.merch-hero-description { + font-size: 1.25rem; + color: #ffffff; + margin-bottom: 2rem; + text-shadow: 1px 1px 3px rgba(0, 0, 0, 0.3); + font-weight: 500; +} + +.merch-hero-stats { + display: flex; + justify-content: center; + gap: 3rem; + flex-wrap: wrap; + margin-top: 2rem; +} + +.stat-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; +} + +.merch-hero .merch-hero-stats .stat-item .stat-number { + font-size: 2rem; + font-weight: 700; + color: #1a1a1a !important; + text-shadow: 1px 1px 2px rgba(255, 255, 255, 0.3) !important; + background: none !important; + -webkit-text-fill-color: #1a1a1a !important; + background-clip: unset !important; + -webkit-background-clip: unset !important; +} + +.stat-label { + font-size: 0.875rem; + color: #ffffff !important; + font-weight: 600; + text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.4); + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Products Section */ +.merch-products-section { + max-width: 1400px; + margin: 0 auto; + padding: 0 2rem; +} + +/* Info Section */ +.merch-info-section { + max-width: 1400px; + margin: 4rem auto 0; + padding: 0 2rem; +} + +.info-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 2rem; + margin-top: 2rem; +} + +.info-card { + padding: 2rem; + background: var(--ifm-card-background-color); + border-radius: 12px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; + text-align: center; +} + +.info-card:hover { + transform: translateY(-5px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15); +} + +.info-card h3 { + font-size: 1.5rem; + margin-bottom: 0.5rem; + color: var(--ifm-color-primary); +} + +.info-card p { + font-size: 1rem; + color: var(--ifm-font-color-secondary); + margin: 0; +} + +/* Floating Cart Button */ +.floating-cart-button { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 60px; + height: 60px; + border-radius: 50%; + background: var(--ifm-color-primary); + color: white; + border: none; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + z-index: 999; + transition: all 0.3s ease; +} + +.floating-cart-button:hover { + transform: scale(1.1); + box-shadow: 0 6px 16px rgba(0, 0, 0, 0.4); +} + +.cart-badge { + position: absolute; + top: -5px; + right: -5px; + background: var(--ifm-color-danger); + color: white; + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; + font-weight: 700; +} + +/* Responsive Design */ +@media (max-width: 768px) { + .merch-hero { + padding: 3rem 1.5rem; + } + + .merch-hero-title { + font-size: 2rem; + } + + .merch-hero-description { + font-size: 1rem; + } + + .merch-hero-stats { + gap: 1.5rem; + } + + .stat-number { + font-size: 1.5rem; + } + + .merch-products-section { + padding: 0 1rem; + } + + .info-grid { + grid-template-columns: 1fr; + gap: 1rem; + } + + .floating-cart-button { + width: 50px; + height: 50px; + bottom: 1rem; + right: 1rem; + } +} + +/* Dark mode adjustments */ +[data-theme="dark"] .merch-hero { + background: linear-gradient( + 135deg, + var(--ifm-color-primary-dark) 0%, + var(--ifm-color-primary-darker) 100% + ); +} + +[data-theme="dark"] .info-card { + background: var(--ifm-background-surface-color); + border: 1px solid var(--ifm-color-emphasis-200); +} diff --git a/static/img/merch/bag.jpeg b/static/img/merch/bag.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..f647e40797bc5447be7b36c11c03352d85e3e3e9 GIT binary patch literal 3204 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 literal 0 HcmV?d00001 diff --git a/static/img/merch/cap.jpeg b/static/img/merch/cap.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..fcb95065316f3bf08dfc79f426a83646c82649e7 GIT binary patch literal 3816 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 literal 0 HcmV?d00001 diff --git a/static/img/merch/hoodie.jpeg b/static/img/merch/hoodie.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..3455470fed324b0e70ef2608c4c03704914b6734 GIT binary patch literal 3710 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 literal 0 HcmV?d00001 diff --git a/static/img/merch/hoodie_2.jpeg b/static/img/merch/hoodie_2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..47f6fc28fd0987768d0c29bcc833a0266a2fb949 GIT binary patch literal 3594 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 Zk2-u|p5rb+pf`; literal 0 HcmV?d00001 diff --git a/static/img/merch/mug.jpeg b/static/img/merch/mug.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..17950775d7200b8fa31c3a7d57014481e318dd2e GIT binary patch literal 2363 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 Q{~uoz@&A8%178>ZF9{}FX8-^I literal 0 HcmV?d00001 diff --git a/static/img/merch/tshirt.jpeg b/static/img/merch/tshirt.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..c06c521ccf10042b91e4950a61e106d56cd212da GIT binary patch literal 4008 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 K`Tq#`c<5hYgiv?@ literal 0 HcmV?d00001 diff --git a/static/img/merch/tshirt_2.jpeg b/static/img/merch/tshirt_2.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..719f0800ee0b838f46e1fc41660469006adff401 GIT binary patch literal 2934 zcmbVNdo$dLwRgKtx2ro5_>X z02l;@Ktv@VV6eoG0mAS%*q$R>MBvJrhBlr7(K7pfKWQ77Twd|KcW-{B*W%Gs(e0<~ zyh;B^DQM{v)lI zAUms_GuFA-yb^l#ZQsgL=wMmynmu+Aw$M%ZFxAFs%SrIDANjYQZoBkfx zrxJpvEhlZs#_dYQd$+Df5xbZ>v9B$kqA~Uvn^$mll zTMPHL~SS7nlx#YQlgx-y6&qq6S(BCI}v7u^UNpy z9w?G)czqz(-W}?`600U<{hl7~;8!&8fZu;!a~bci-=?oF|Bn$c`Z_>&+OcO27;GUtZpv`G!trj_()61!kjmzrDIw#SjkNzb_HE`y$3qWM#6W=|;sV9WDh z@78a6wz*i@aEkbP>;p>lGDO+1;DNqz5Mg;k05mc20w9D-ou;dBXH^p++zzE&CB_UT z;7L_jnhZNMXFI~hw37RGb&1%j%`KYAx6-To?TK?K>qEK@0$@M@cyo!~{c#_`iVBgc zY}wT>%E2P16V*XekZvP$s3zC|9(XAy2Xzt^kmC-I8PmorVjFwM1s$PBhYc-TN>D)D=?HhV!ta%5c1^SZ^_gfS9bZXW$G<5a2;!k=<^I%j!Q3;I0stN{(e?sr zshYLlxisOAeuKROESQhz!|(~1rlQ=%M9CfacpuRm=+W~#uIkyJuv_4k(?}Y1XN9y# ze$WejIyidJ8-4qY=*or|GHKc*G#VLH%{0>;3FyhG!}v|HVpkb=zaP^lkLnIgIi#iK zV`2$h{(U1+kxsK_9`RUu%?#15lUd_$vqUM!rsPD4G<61bxDb%hi5+f zMLQ;7aDzSB)d!8V`r>Wly;4ln;I76zt?xblPtraI*%qsu21Ec3Q#`?md-Gsd{;=8amXr#PP=sPdi{+KBR6%@3qE6jybo^fAv0Wi{t?T{|Hz~Jkg$qCu zZvKJ+jivM!hejClG6<<-9aH?cpDkPiLXewjmyM|CiNob86BGK@8~Oc;?N}K3?`SJNXg-dbk|1SvKlK{ z>(_#~k6?NB>$A<>dp-B)r_a3vL=}HQ0yy}q7T7QHlbQ6evc|QZ)P`L0acGuKV}0k> zimFM++2M{nR@Bb&`Df2@uPsaK_74)Utkrqt{tQV3Hp%JTNh<9X21jtLt)MzSoiwac zquqNhKFe_1;*&mo?S@%)VY1!aHiQ=G#Yza%YtVh!Ndd_}{#ouiJ1WDdo$xfKp~qm| zgNuK0a`eB0+f~47<&m-^RL6SA4gSoTiDeyjJ@?xWXkD)!)AHSXomg4K+aj-b_YGuL z4T+CitA8*G zKwPnF>SYYG^i<6NrFv=4u-=I$dCoTI1Bh!tM@YVTUEApJ%Lg&#so>#p!w_n~(w_^} ziM5iHz+G8ok<J0JPuzf&*L(UwxTt$u5<_ZX9&%*n}GA8j3MiP!|1}#N%;R4W{&bg&!V@8s<&FITK9F z&aUcYKT~2!q87T!BwFqntsd-LCkKz?-NMe2#oyy@Ey*7KuILKQU-rFr8SS|t># Date: Sun, 5 Oct 2025 23:13:51 -0400 Subject: [PATCH 2/3] Add Shopify integration to merch store --- .env.example | 6 + docusaurus.config.ts | 12 +- src/lib/shopify.ts | 479 ++++++++++++++++++++++++++++++++++++++ src/pages/merch/index.tsx | 72 +++++- 4 files changed, 561 insertions(+), 8 deletions(-) create mode 100644 src/lib/shopify.ts diff --git a/.env.example b/.env.example index ee400852..9c009e41 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,12 @@ # No special permissions needed for public repositories GITHUB_TOKEN=your_github_token_here +# Shopify Configuration (for Merch Store) +# Get these from: Shopify Admin > Settings > Apps and sales channels > Develop apps +# Required scopes: unauthenticated_read_product_listings, unauthenticated_write_checkouts +SHOPIFY_STORE_DOMAIN=your-store.myshopify.com +SHOPIFY_STOREFRONT_ACCESS_TOKEN=your_storefront_access_token_here + # Firebase Configuration (if needed) # FIREBASE_API_KEY=your_firebase_api_key # FIREBASE_AUTH_DOMAIN=your_firebase_auth_domain diff --git a/docusaurus.config.ts b/docusaurus.config.ts index c3ca63ca..ad48b898 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -156,11 +156,6 @@ const config: Config = { html: '💰 Donate', position: "left", }, - { - to: "/merch", - html: '🛍️ Merch', - position: "left", - }, { type: "dropdown", html: '👩🏻‍💻 Devfolio', @@ -206,6 +201,10 @@ const config: Config = { label: "🎙️ Podcast", to: "/podcasts/", }, + { + label: "🛍️ Merch Store", + to: "/merch", + }, ], }, // Search disabled until Algolia is properly configured @@ -270,6 +269,9 @@ const config: Config = { // ✅ Add this customFields object to expose the token to the client-side customFields: { gitToken: process.env.DOCUSAURUS_GIT_TOKEN, + // Shopify credentials for merch store + SHOPIFY_STORE_DOMAIN: process.env.SHOPIFY_STORE_DOMAIN || 'junh9v-gw.myshopify.com', + SHOPIFY_STOREFRONT_ACCESS_TOKEN: process.env.SHOPIFY_STOREFRONT_ACCESS_TOKEN || '2503dfbf93132b42e627e7d53b3ba3e9', hooks: { onBrokenMarkdownLinks: "warn", }, diff --git a/src/lib/shopify.ts b/src/lib/shopify.ts new file mode 100644 index 00000000..8b51c909 --- /dev/null +++ b/src/lib/shopify.ts @@ -0,0 +1,479 @@ +/** + * Shopify Integration for Recode Merch Store + * + * This file provides functions to interact with Shopify's Storefront API + * to fetch products, create carts, and process checkouts. + * + * Setup Instructions: + * 1. Create a Shopify store at https://shopify.com + * 2. Go to Settings > Apps and sales channels > Develop apps + * 3. Create a new app and get your Storefront API access token + * 4. Add your credentials to docusaurus.config.ts customFields + * + * For development, add these to your docusaurus.config.ts: + * customFields: { + * SHOPIFY_STORE_DOMAIN: 'your-store.myshopify.com', + * SHOPIFY_STOREFRONT_ACCESS_TOKEN: 'your-token-here', + * } + */ + +// Get credentials from Docusaurus customFields (configured in docusaurus.config.ts) +// These are set at build time and won't expose credentials in the code +const SHOPIFY_STORE_DOMAIN = + (typeof window !== 'undefined' && (window as any).docusaurus?.siteConfig?.customFields?.SHOPIFY_STORE_DOMAIN as string) || ''; +const SHOPIFY_STOREFRONT_ACCESS_TOKEN = + (typeof window !== 'undefined' && (window as any).docusaurus?.siteConfig?.customFields?.SHOPIFY_STOREFRONT_ACCESS_TOKEN as string) || ''; + +const SHOPIFY_GRAPHQL_URL = `https://${SHOPIFY_STORE_DOMAIN}/api/2024-01/graphql.json`; + +interface ShopifyProduct { + id: string; + title: string; + description: string; + handle: string; + priceRange: { + minVariantPrice: { + amount: string; + currencyCode: string; + }; + }; + images: { + edges: Array<{ + node: { + url: string; + altText: string; + }; + }>; + }; + variants: { + edges: Array<{ + node: { + id: string; + title: string; + priceV2: { + amount: string; + currencyCode: string; + }; + availableForSale: boolean; + }; + }>; + }; +} + +interface ShopifyCheckout { + id: string; + webUrl: string; + lineItems: { + edges: Array<{ + node: { + id: string; + title: string; + quantity: number; + variant: { + id: string; + title: string; + priceV2: { + amount: string; + currencyCode: string; + }; + image: { + url: string; + }; + }; + }; + }>; + }; + subtotalPriceV2: { + amount: string; + currencyCode: string; + }; + totalPriceV2: { + amount: string; + currencyCode: string; + }; +} + +/** + * Make a request to Shopify's Storefront API + */ +async function shopifyFetch(query: string, variables = {}): Promise { + if (!SHOPIFY_STORE_DOMAIN || !SHOPIFY_STOREFRONT_ACCESS_TOKEN) { + console.warn('Shopify credentials not configured. Using mock data.'); + throw new Error('Shopify not configured'); + } + + const response = await fetch(SHOPIFY_GRAPHQL_URL, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Shopify-Storefront-Access-Token': SHOPIFY_STOREFRONT_ACCESS_TOKEN, + }, + body: JSON.stringify({ query, variables }), + }); + + if (!response.ok) { + throw new Error(`Shopify API error: ${response.statusText}`); + } + + const json = await response.json(); + + if (json.errors) { + throw new Error(`Shopify GraphQL errors: ${JSON.stringify(json.errors)}`); + } + + return json.data; +} + +/** + * Fetch all products from Shopify + */ +export async function getProducts(first = 20): Promise { + const query = ` + query GetProducts($first: Int!) { + products(first: $first) { + edges { + node { + id + title + description + handle + priceRange { + minVariantPrice { + amount + currencyCode + } + } + images(first: 1) { + edges { + node { + url + altText + } + } + } + variants(first: 10) { + edges { + node { + id + title + priceV2 { + amount + currencyCode + } + availableForSale + } + } + } + } + } + } + } + `; + + try { + const data = await shopifyFetch<{ products: { edges: Array<{ node: ShopifyProduct }> } }>( + query, + { first } + ); + return data.products.edges.map((edge) => edge.node); + } catch (error) { + console.error('Error fetching products:', error); + return []; + } +} + +/** + * Get a single product by handle + */ +export async function getProductByHandle(handle: string): Promise { + const query = ` + query GetProduct($handle: String!) { + productByHandle(handle: $handle) { + id + title + description + handle + priceRange { + minVariantPrice { + amount + currencyCode + } + } + images(first: 5) { + edges { + node { + url + altText + } + } + } + variants(first: 10) { + edges { + node { + id + title + priceV2 { + amount + currencyCode + } + availableForSale + } + } + } + } + } + `; + + try { + const data = await shopifyFetch<{ productByHandle: ShopifyProduct }>(query, { handle }); + return data.productByHandle; + } catch (error) { + console.error('Error fetching product:', error); + return null; + } +} + +/** + * Create a new checkout + */ +export async function createCheckout(): Promise { + const query = ` + mutation CreateCheckout { + checkoutCreate(input: {}) { + checkout { + id + webUrl + } + checkoutUserErrors { + field + message + } + } + } + `; + + try { + const data = await shopifyFetch<{ + checkoutCreate: { + checkout: ShopifyCheckout; + checkoutUserErrors: Array<{ field: string; message: string }>; + }; + }>(query); + + if (data.checkoutCreate.checkoutUserErrors.length > 0) { + console.error('Checkout errors:', data.checkoutCreate.checkoutUserErrors); + return null; + } + + return data.checkoutCreate.checkout; + } catch (error) { + console.error('Error creating checkout:', error); + return null; + } +} + +/** + * Add items to checkout + */ +export async function addToCheckout( + checkoutId: string, + lineItems: Array<{ variantId: string; quantity: number }> +): Promise { + const query = ` + mutation AddToCheckout($checkoutId: ID!, $lineItems: [CheckoutLineItemInput!]!) { + checkoutLineItemsAdd(checkoutId: $checkoutId, lineItems: $lineItems) { + checkout { + id + webUrl + lineItems(first: 10) { + edges { + node { + id + title + quantity + variant { + id + title + priceV2 { + amount + currencyCode + } + image { + url + } + } + } + } + } + subtotalPriceV2 { + amount + currencyCode + } + totalPriceV2 { + amount + currencyCode + } + } + checkoutUserErrors { + field + message + } + } + } + `; + + try { + const data = await shopifyFetch<{ + checkoutLineItemsAdd: { + checkout: ShopifyCheckout; + checkoutUserErrors: Array<{ field: string; message: string }>; + }; + }>(query, { + checkoutId, + lineItems, + }); + + if (data.checkoutLineItemsAdd.checkoutUserErrors.length > 0) { + console.error('Add to checkout errors:', data.checkoutLineItemsAdd.checkoutUserErrors); + return null; + } + + return data.checkoutLineItemsAdd.checkout; + } catch (error) { + console.error('Error adding to checkout:', error); + return null; + } +} + +/** + * Update line item quantities in checkout + */ +export async function updateCheckoutLineItems( + checkoutId: string, + lineItems: Array<{ id: string; quantity: number }> +): Promise { + const query = ` + mutation UpdateCheckout($checkoutId: ID!, $lineItems: [CheckoutLineItemUpdateInput!]!) { + checkoutLineItemsUpdate(checkoutId: $checkoutId, lineItems: $lineItems) { + checkout { + id + webUrl + lineItems(first: 10) { + edges { + node { + id + title + quantity + } + } + } + subtotalPriceV2 { + amount + currencyCode + } + totalPriceV2 { + amount + currencyCode + } + } + checkoutUserErrors { + field + message + } + } + } + `; + + try { + const data = await shopifyFetch<{ + checkoutLineItemsUpdate: { + checkout: ShopifyCheckout; + checkoutUserErrors: Array<{ field: string; message: string }>; + }; + }>(query, { + checkoutId, + lineItems, + }); + + if (data.checkoutLineItemsUpdate.checkoutUserErrors.length > 0) { + console.error('Update checkout errors:', data.checkoutLineItemsUpdate.checkoutUserErrors); + return null; + } + + return data.checkoutLineItemsUpdate.checkout; + } catch (error) { + console.error('Error updating checkout:', error); + return null; + } +} + +/** + * Remove line items from checkout + */ +export async function removeFromCheckout( + checkoutId: string, + lineItemIds: string[] +): Promise { + const query = ` + mutation RemoveFromCheckout($checkoutId: ID!, $lineItemIds: [ID!]!) { + checkoutLineItemsRemove(checkoutId: $checkoutId, lineItemIds: $lineItemIds) { + checkout { + id + webUrl + lineItems(first: 10) { + edges { + node { + id + title + quantity + } + } + } + subtotalPriceV2 { + amount + currencyCode + } + totalPriceV2 { + amount + currencyCode + } + } + checkoutUserErrors { + field + message + } + } + } + `; + + try { + const data = await shopifyFetch<{ + checkoutLineItemsRemove: { + checkout: ShopifyCheckout; + checkoutUserErrors: Array<{ field: string; message: string }>; + }; + }>(query, { + checkoutId, + lineItemIds, + }); + + if (data.checkoutLineItemsRemove.checkoutUserErrors.length > 0) { + console.error('Remove from checkout errors:', data.checkoutLineItemsRemove.checkoutUserErrors); + return null; + } + + return data.checkoutLineItemsRemove.checkout; + } catch (error) { + console.error('Error removing from checkout:', error); + return null; + } +} + +/** + * Check if Shopify is configured + */ +export function isShopifyConfigured(): boolean { + return Boolean(SHOPIFY_STORE_DOMAIN && SHOPIFY_STOREFRONT_ACCESS_TOKEN); +} + +export type { ShopifyProduct, ShopifyCheckout }; diff --git a/src/pages/merch/index.tsx b/src/pages/merch/index.tsx index 2ad4c0ad..9c15096f 100644 --- a/src/pages/merch/index.tsx +++ b/src/pages/merch/index.tsx @@ -6,6 +6,7 @@ import ProductGrid from "../../components/merch/ProductGrid"; import FilterBar from "../../components/merch/FilterBar"; import ShoppingCart from "../../components/merch/ShoppingCart"; import { ShoppingBag } from "lucide-react"; +import { getProducts, isShopifyConfigured } from "../../lib/shopify"; import "./merch.css"; export interface Product { @@ -85,12 +86,69 @@ const sampleProducts: Product[] = [ export default function MerchPage(): ReactNode { const [selectedCategory, setSelectedCategory] = useState("all"); const [sortBy, setSortBy] = useState("featured"); + const [products, setProducts] = useState(sampleProducts); const [filteredProducts, setFilteredProducts] = useState(sampleProducts); const [cartOpen, setCartOpen] = useState(false); const [cartItems, setCartItems] = useState>([]); + const [loading, setLoading] = useState(true); + // Fetch products from Shopify on mount useEffect(() => { - let filtered = [...sampleProducts]; + async function fetchShopifyProducts() { + try { + if (isShopifyConfigured()) { + console.log('Fetching products from Shopify...'); + const shopifyProducts = await getProducts(20); + + if (shopifyProducts && shopifyProducts.length > 0) { + // Convert Shopify products to our Product interface + const formattedProducts: Product[] = shopifyProducts.map((p) => { + const imageUrl = p.images.edges[0]?.node.url || ''; + const price = parseFloat(p.priceRange.minVariantPrice.amount); + + return { + id: p.id, + title: p.title, + description: p.description || '', + price: price, + image: imageUrl, + category: 'accessories', // Default category, you can use Shopify tags + shopifyId: p.id, + variants: { + size: p.variants.edges.map(v => v.node.title).filter(t => t !== 'Default Title'), + }, + }; + }); + + console.log('Loaded products from Shopify:', formattedProducts.length); + setProducts(formattedProducts); + setFilteredProducts(formattedProducts); + } else { + console.log('No products found in Shopify, using sample products'); + setProducts(sampleProducts); + setFilteredProducts(sampleProducts); + } + } else { + console.log('Shopify not configured, using sample products'); + setProducts(sampleProducts); + setFilteredProducts(sampleProducts); + } + } catch (error) { + console.error('Error fetching products from Shopify:', error); + // Fallback to sample products on error + setProducts(sampleProducts); + setFilteredProducts(sampleProducts); + } finally { + setLoading(false); + } + } + + fetchShopifyProducts(); + }, []); + + // Filter and sort products + useEffect(() => { + let filtered = [...products]; // Filter by category if (selectedCategory !== "all") { @@ -114,7 +172,7 @@ export default function MerchPage(): ReactNode { } setFilteredProducts(filtered); - }, [selectedCategory, sortBy]); + }, [selectedCategory, sortBy, products]); const addToCart = (product: Product) => { setCartItems((prev) => { @@ -194,7 +252,15 @@ export default function MerchPage(): ReactNode { {/* Products Grid */}
- + {loading ? ( +
+

+ Loading products... +

+
+ ) : ( + + )}
{/* Shopping Cart */} From a440db40ecdcbdc9be9faa2c1f2d272b100c898a Mon Sep 17 00:00:00 2001 From: shreyas Date: Sun, 5 Oct 2025 23:23:18 -0400 Subject: [PATCH 3/3] Fix Shopify configuration to use @generated/docusaurus.config --- src/lib/shopify.ts | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/lib/shopify.ts b/src/lib/shopify.ts index 8b51c909..55954bd4 100644 --- a/src/lib/shopify.ts +++ b/src/lib/shopify.ts @@ -8,23 +8,20 @@ * 1. Create a Shopify store at https://shopify.com * 2. Go to Settings > Apps and sales channels > Develop apps * 3. Create a new app and get your Storefront API access token - * 4. Add your credentials to docusaurus.config.ts customFields - * - * For development, add these to your docusaurus.config.ts: - * customFields: { - * SHOPIFY_STORE_DOMAIN: 'your-store.myshopify.com', - * SHOPIFY_STOREFRONT_ACCESS_TOKEN: 'your-token-here', - * } + * 4. Credentials are configured in docusaurus.config.ts customFields */ -// Get credentials from Docusaurus customFields (configured in docusaurus.config.ts) -// These are set at build time and won't expose credentials in the code -const SHOPIFY_STORE_DOMAIN = - (typeof window !== 'undefined' && (window as any).docusaurus?.siteConfig?.customFields?.SHOPIFY_STORE_DOMAIN as string) || ''; -const SHOPIFY_STOREFRONT_ACCESS_TOKEN = - (typeof window !== 'undefined' && (window as any).docusaurus?.siteConfig?.customFields?.SHOPIFY_STOREFRONT_ACCESS_TOKEN as string) || ''; +import siteConfig from '@generated/docusaurus.config'; -const SHOPIFY_GRAPHQL_URL = `https://${SHOPIFY_STORE_DOMAIN}/api/2024-01/graphql.json`; +// Get credentials from Docusaurus customFields +function getShopifyConfig() { + const domain = (siteConfig.customFields?.SHOPIFY_STORE_DOMAIN as string) || ''; + const token = (siteConfig.customFields?.SHOPIFY_STOREFRONT_ACCESS_TOKEN as string) || ''; + + return { domain, token }; +} + +const SHOPIFY_GRAPHQL_URL = (domain: string) => `https://${domain}/api/2024-01/graphql.json`; interface ShopifyProduct { id: string; @@ -97,16 +94,18 @@ interface ShopifyCheckout { * Make a request to Shopify's Storefront API */ async function shopifyFetch(query: string, variables = {}): Promise { - if (!SHOPIFY_STORE_DOMAIN || !SHOPIFY_STOREFRONT_ACCESS_TOKEN) { + const config = getShopifyConfig(); + + if (!config.domain || !config.token) { console.warn('Shopify credentials not configured. Using mock data.'); throw new Error('Shopify not configured'); } - const response = await fetch(SHOPIFY_GRAPHQL_URL, { + const response = await fetch(SHOPIFY_GRAPHQL_URL(config.domain), { method: 'POST', headers: { 'Content-Type': 'application/json', - 'X-Shopify-Storefront-Access-Token': SHOPIFY_STOREFRONT_ACCESS_TOKEN, + 'X-Shopify-Storefront-Access-Token': config.token, }, body: JSON.stringify({ query, variables }), }); @@ -473,7 +472,8 @@ export async function removeFromCheckout( * Check if Shopify is configured */ export function isShopifyConfigured(): boolean { - return Boolean(SHOPIFY_STORE_DOMAIN && SHOPIFY_STOREFRONT_ACCESS_TOKEN); + const config = getShopifyConfig(); + return Boolean(config.domain && config.token); } export type { ShopifyProduct, ShopifyCheckout };