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 513d07ec..ad48b898 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -201,6 +201,10 @@ const config: Config = { label: "🎙️ Podcast", to: "/podcasts/", }, + { + label: "🛍️ Merch Store", + to: "/merch", + }, ], }, // Search disabled until Algolia is properly configured @@ -265,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/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/lib/shopify.ts b/src/lib/shopify.ts new file mode 100644 index 00000000..55954bd4 --- /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. Credentials are configured in docusaurus.config.ts customFields + */ + +import siteConfig from '@generated/docusaurus.config'; + +// 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; + 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 { + 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(config.domain), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Shopify-Storefront-Access-Token': config.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 { + const config = getShopifyConfig(); + return Boolean(config.domain && config.token); +} + +export type { ShopifyProduct, ShopifyCheckout }; diff --git a/src/pages/merch/index.tsx b/src/pages/merch/index.tsx new file mode 100644 index 00000000..9c15096f --- /dev/null +++ b/src/pages/merch/index.tsx @@ -0,0 +1,333 @@ +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 { getProducts, isShopifyConfigured } from "../../lib/shopify"; +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 [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(() => { + 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") { + 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, products]); + + 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 */} +
+ {loading ? ( +
+

+ Loading products... +

+
+ ) : ( + + )} +
+ + {/* 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 00000000..f647e407 Binary files /dev/null and b/static/img/merch/bag.jpeg differ diff --git a/static/img/merch/cap.jpeg b/static/img/merch/cap.jpeg new file mode 100644 index 00000000..fcb95065 Binary files /dev/null and b/static/img/merch/cap.jpeg differ diff --git a/static/img/merch/hoodie.jpeg b/static/img/merch/hoodie.jpeg new file mode 100644 index 00000000..3455470f Binary files /dev/null and b/static/img/merch/hoodie.jpeg differ diff --git a/static/img/merch/hoodie_2.jpeg b/static/img/merch/hoodie_2.jpeg new file mode 100644 index 00000000..47f6fc28 Binary files /dev/null and b/static/img/merch/hoodie_2.jpeg differ diff --git a/static/img/merch/mug.jpeg b/static/img/merch/mug.jpeg new file mode 100644 index 00000000..17950775 Binary files /dev/null and b/static/img/merch/mug.jpeg differ diff --git a/static/img/merch/sticker.jpeg b/static/img/merch/sticker.jpeg new file mode 100644 index 00000000..634ede5e Binary files /dev/null and b/static/img/merch/sticker.jpeg differ diff --git a/static/img/merch/tshirt.jpeg b/static/img/merch/tshirt.jpeg new file mode 100644 index 00000000..c06c521c Binary files /dev/null and b/static/img/merch/tshirt.jpeg differ diff --git a/static/img/merch/tshirt_2.jpeg b/static/img/merch/tshirt_2.jpeg new file mode 100644 index 00000000..719f0800 Binary files /dev/null and b/static/img/merch/tshirt_2.jpeg differ