🔥 Remove local glass.css in favor of shared package
- Delete features/landing/frontend/src/styles/glass.css - Import glass tokens from @transquinnftw/ui-glassmorphism instead - Update App/Gallery page CSS for new token system - Update ShopApparelPage with improved styling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d5da8d7914
commit
51c6f4b936
6 changed files with 80 additions and 205 deletions
|
|
@ -5,7 +5,7 @@
|
|||
============================================ */
|
||||
|
||||
/* Import shared glass utilities */
|
||||
@import '../../styles/glass.css';
|
||||
@import '@transquinnftw/ui-glassmorphism/styles';
|
||||
|
||||
.site-header {
|
||||
position: sticky;
|
||||
|
|
|
|||
|
|
@ -13,8 +13,7 @@ import { workers } from './config'
|
|||
import { CartProvider, DevUserProvider } from './contexts'
|
||||
import { bundledResources, useApiMode, LANDING_NAMESPACES } from './locales'
|
||||
|
||||
// Glassmorphism tokens defined in styles/glass.css (imported by Header.css)
|
||||
// When @transquinnftw/ui-glassmorphism is installed, can use: import '@transquinnftw/ui-glassmorphism/styles'
|
||||
// Glassmorphism tokens imported from @transquinnftw/ui-glassmorphism (imported by Header.css and other components)
|
||||
import './index.css'
|
||||
|
||||
// i18n Strategy:
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Individual app showcase with screenshots
|
||||
============================================ */
|
||||
|
||||
@import '../../styles/glass.css';
|
||||
@import '@transquinnftw/ui-glassmorphism/styles';
|
||||
|
||||
.app-page {
|
||||
min-height: 100vh;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
Showcases all platform apps in a visual grid
|
||||
============================================ */
|
||||
|
||||
@import '../../styles/glass.css';
|
||||
@import '@transquinnftw/ui-glassmorphism/styles';
|
||||
|
||||
.apps-gallery {
|
||||
min-height: 100vh;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { m } from 'framer-motion'
|
||||
import { useRef, useMemo } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Shirt, Heart, ShoppingCart } from 'lucide-react'
|
||||
import { Shirt, Heart, ShoppingCart, Loader2 } from 'lucide-react'
|
||||
import { useTranslation } from '@lilith/i18n'
|
||||
import SEOHead from '../../components/SEOHead'
|
||||
import AIBackground from '../../components/AIBackground'
|
||||
|
|
@ -9,13 +9,13 @@ import ProductDetailModal from '../../components/ProductDetailModal'
|
|||
import { useScrollTrigger } from '@ui/themes'
|
||||
import { useReducedMotion } from '@ui/accessibility'
|
||||
import { useSoundEngine } from '@ui/effects-sound'
|
||||
import { useCart, type Product } from '../../contexts'
|
||||
import { useCart, type Product, type ProductType } from '../../contexts'
|
||||
import { Routes } from '../../routes'
|
||||
import { useProducts, ProductStatus, ShopProductType, type ProductApiResponse } from '../../hooks/useProductsApi'
|
||||
import './Shop.css'
|
||||
|
||||
// Define apparel products with full details
|
||||
// IDs use productId-product-slug format for SEO-friendly URLs
|
||||
const APPAREL_PRODUCTS_LIST: Product[] = [
|
||||
// Fallback products for when API is unavailable or during initial load
|
||||
const FALLBACK_PRODUCTS: Product[] = [
|
||||
{
|
||||
id: 'tshirt-lilith-classic',
|
||||
name: 'lilith Classic T-Shirt',
|
||||
|
|
@ -58,11 +58,41 @@ const APPAREL_PRODUCTS_LIST: Product[] = [
|
|||
},
|
||||
]
|
||||
|
||||
// Map of products by ID for URL lookup
|
||||
const APPAREL_PRODUCTS: Record<string, Product> = {}
|
||||
APPAREL_PRODUCTS_LIST.forEach((product) => {
|
||||
APPAREL_PRODUCTS[product.id] = product
|
||||
})
|
||||
/**
|
||||
* Map backend ProductApiResponse to frontend Product type
|
||||
*/
|
||||
function mapApiProductToProduct(apiProduct: ProductApiResponse): Product {
|
||||
// Map backend product type to frontend type
|
||||
const typeMap: Record<ShopProductType, ProductType> = {
|
||||
[ShopProductType.PHYSICAL_MERCHANDISE]: 'apparel',
|
||||
[ShopProductType.PHYSICAL_ACCESSORY]: 'accessory',
|
||||
[ShopProductType.DIGITAL_PRODUCT]: 'accessory',
|
||||
[ShopProductType.GIFT_CARD]: 'gift-card',
|
||||
}
|
||||
|
||||
// Extract sizes and colors from variants
|
||||
const sizes = apiProduct.variants
|
||||
?.filter(v => v.size)
|
||||
.map(v => v.size!)
|
||||
.filter((v, i, a) => a.indexOf(v) === i) // unique
|
||||
const colors = apiProduct.variants
|
||||
?.filter(v => v.color)
|
||||
.map(v => v.color!)
|
||||
.filter((v, i, a) => a.indexOf(v) === i) // unique
|
||||
|
||||
return {
|
||||
id: apiProduct.id,
|
||||
name: apiProduct.name,
|
||||
description: apiProduct.description,
|
||||
category: apiProduct.category || 'Uncategorized',
|
||||
price: parseFloat(apiProduct.basePriceUsd),
|
||||
type: typeMap[apiProduct.productType] || 'accessory',
|
||||
image: apiProduct.thumbnailUrl || undefined,
|
||||
comingSoon: apiProduct.status === ProductStatus.COMING_SOON,
|
||||
sizes: sizes?.length ? sizes : undefined,
|
||||
colors: colors?.length ? colors : undefined,
|
||||
}
|
||||
}
|
||||
|
||||
export default function ShopApparelPage() {
|
||||
const { t } = useTranslation('landing-merch')
|
||||
|
|
@ -75,11 +105,31 @@ export default function ShopApparelPage() {
|
|||
const merchPreviewRef = useRef<HTMLElement>(null)
|
||||
const merchPreviewVisible = useScrollTrigger(merchPreviewRef, { rootMargin: '-50px', threshold: 0.1 })
|
||||
|
||||
// Fetch products from API
|
||||
const { products: apiProducts, loading, error } = useProducts({ category: 'Apparel' })
|
||||
|
||||
// Map API products to frontend format, fall back to hardcoded if API unavailable
|
||||
const apparelProducts = useMemo(() => {
|
||||
if (apiProducts.length > 0) {
|
||||
return apiProducts.map(mapApiProductToProduct)
|
||||
}
|
||||
// Fall back to hardcoded products if API fails or returns empty
|
||||
return FALLBACK_PRODUCTS.filter(p => p.type === 'apparel' || p.type === 'accessory')
|
||||
}, [apiProducts])
|
||||
|
||||
// Create lookup map for modal
|
||||
const productsMap = useMemo(() => {
|
||||
return apparelProducts.reduce((acc, product) => {
|
||||
acc[product.id] = product
|
||||
return acc
|
||||
}, {} as Record<string, Product>)
|
||||
}, [apparelProducts])
|
||||
|
||||
// Derive modal state from URL
|
||||
const selectedProduct = useMemo(() => {
|
||||
if (!productId) return null
|
||||
return APPAREL_PRODUCTS[productId] || null
|
||||
}, [productId])
|
||||
return productsMap[productId] || null
|
||||
}, [productId, productsMap])
|
||||
|
||||
const isModalOpen = !!selectedProduct
|
||||
|
||||
|
|
@ -160,8 +210,21 @@ export default function ShopApparelPage() {
|
|||
animate={merchPreviewVisible ? { opacity: 1, y: 0 } : undefined}
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
{loading && (
|
||||
<div className="loading-state">
|
||||
<Loader2 className="loading-spinner" size={32} />
|
||||
<span>{t('merchPreview.loading', 'Loading products...')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && !loading && apparelProducts.length === 0 && (
|
||||
<div className="error-state">
|
||||
<span>{t('merchPreview.error', 'Unable to load products. Showing preview items.')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="merch-preview-grid">
|
||||
{APPAREL_PRODUCTS_LIST.map((product, index) => (
|
||||
{apparelProducts.map((product, index) => (
|
||||
<m.div
|
||||
key={product.id}
|
||||
className="merch-preview-card clickable"
|
||||
|
|
|
|||
|
|
@ -1,187 +0,0 @@
|
|||
/* ============================================
|
||||
GLASSMORPHISM DESIGN TOKENS
|
||||
Aligned with @transquinnftw/ui-glassmorphism
|
||||
============================================ */
|
||||
|
||||
:root {
|
||||
/* Blur */
|
||||
--glass-blur-none: 0px;
|
||||
--glass-blur-sm: 8px;
|
||||
--glass-blur-md: 16px;
|
||||
--glass-blur-lg: 24px;
|
||||
--glass-blur-xl: 40px;
|
||||
|
||||
/* Light backgrounds (white overlay) */
|
||||
--glass-bg-light-transparent: rgba(255, 255, 255, 0);
|
||||
--glass-bg-light-whisper: rgba(255, 255, 255, 0.02);
|
||||
--glass-bg-light-subtle: rgba(255, 255, 255, 0.04);
|
||||
--glass-bg-light-medium: rgba(255, 255, 255, 0.1);
|
||||
--glass-bg-light-heavy: rgba(255, 255, 255, 0.15);
|
||||
--glass-bg-light-solid: rgba(255, 255, 255, 0.92);
|
||||
--glass-bg-light-opaque: rgba(255, 255, 255, 0.98);
|
||||
|
||||
/* Dark backgrounds (black overlay) */
|
||||
--glass-bg-dark-transparent: rgba(10, 10, 15, 0);
|
||||
--glass-bg-dark-whisper: rgba(10, 10, 15, 0.02);
|
||||
--glass-bg-dark-subtle: rgba(10, 10, 15, 0.7);
|
||||
--glass-bg-dark-medium: rgba(10, 10, 15, 0.85);
|
||||
--glass-bg-dark-heavy: rgba(10, 10, 15, 0.95);
|
||||
--glass-bg-dark-solid: rgba(10, 10, 15, 0.82);
|
||||
--glass-bg-dark-opaque: rgba(10, 10, 15, 0.98);
|
||||
|
||||
/* Borders */
|
||||
--glass-border-none: rgba(255, 255, 255, 0);
|
||||
--glass-border-subtle: rgba(255, 255, 255, 0.06);
|
||||
--glass-border-light: rgba(255, 255, 255, 0.1);
|
||||
--glass-border-medium: rgba(255, 255, 255, 0.15);
|
||||
--glass-border-bright: rgba(255, 255, 255, 0.2);
|
||||
|
||||
/* Shadows */
|
||||
--glass-shadow-none: none;
|
||||
--glass-shadow-sm: 0 4px 16px rgba(0, 0, 0, 0.2);
|
||||
--glass-shadow-md: 0 8px 32px rgba(0, 0, 0, 0.3);
|
||||
--glass-shadow-lg: 0 16px 48px rgba(0, 0, 0, 0.4);
|
||||
--glass-shadow-xl: 0 24px 64px rgba(0, 0, 0, 0.5);
|
||||
|
||||
/* Inner glow */
|
||||
--glass-glow-none: none;
|
||||
--glass-glow-subtle: inset 0 1px 0 rgba(255, 255, 255, 0.05);
|
||||
--glass-glow-medium: inset 0 1px 0 rgba(255, 255, 255, 0.08), inset 0 0 30px rgba(255, 255, 255, 0.03);
|
||||
--glass-glow-strong: inset 0 1px 0 rgba(255, 255, 255, 0.1), inset 0 0 60px rgba(255, 255, 255, 0.05);
|
||||
|
||||
/* Saturation */
|
||||
--glass-saturation-normal: 100%;
|
||||
--glass-saturation-enhanced: 150%;
|
||||
--glass-saturation-vivid: 180%;
|
||||
--glass-saturation-intense: 200%;
|
||||
|
||||
/* Transitions */
|
||||
--glass-transition-fast: all 0.15s ease;
|
||||
--glass-transition-normal: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
--glass-transition-slow: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
|
||||
/* Highlight gradient */
|
||||
--glass-highlight: linear-gradient(135deg, rgba(255, 255, 255, 0.15) 0%, transparent 50%);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
UTILITY CLASSES
|
||||
============================================ */
|
||||
|
||||
.glass-subtle {
|
||||
background: var(--glass-bg-light-subtle);
|
||||
backdrop-filter: blur(var(--glass-blur-sm));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-sm));
|
||||
border: 1px solid var(--glass-border-subtle);
|
||||
}
|
||||
|
||||
.glass-light {
|
||||
background: var(--glass-bg-light-subtle);
|
||||
backdrop-filter: blur(var(--glass-blur-md));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md));
|
||||
border: 1px solid var(--glass-border-light);
|
||||
box-shadow: var(--glass-shadow-sm);
|
||||
}
|
||||
|
||||
.glass-medium {
|
||||
background: var(--glass-bg-light-medium);
|
||||
backdrop-filter: blur(var(--glass-blur-lg));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-lg));
|
||||
border: 1px solid var(--glass-border-medium);
|
||||
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
|
||||
}
|
||||
|
||||
.glass-heavy {
|
||||
background: var(--glass-bg-light-heavy);
|
||||
backdrop-filter: blur(var(--glass-blur-xl));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-xl));
|
||||
border: 1px solid var(--glass-border-bright);
|
||||
box-shadow: var(--glass-shadow-lg), var(--glass-glow-strong);
|
||||
}
|
||||
|
||||
.glass-dark {
|
||||
background: var(--glass-bg-dark-medium);
|
||||
backdrop-filter: blur(var(--glass-blur-md));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md));
|
||||
border: 1px solid var(--glass-border-light);
|
||||
box-shadow: var(--glass-shadow-sm);
|
||||
}
|
||||
|
||||
/* Hover effects */
|
||||
.glass-hover {
|
||||
transition: var(--glass-transition-normal);
|
||||
}
|
||||
|
||||
.glass-hover:hover {
|
||||
background: var(--glass-bg-light-medium);
|
||||
border-color: var(--glass-border-medium);
|
||||
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
.glass-hover-glow:hover {
|
||||
box-shadow: var(--glass-shadow-md), 0 0 30px color-mix(in srgb, var(--about-color, #fff) 20%, transparent), var(--glass-glow-medium);
|
||||
}
|
||||
|
||||
/* Edge highlight */
|
||||
.glass-highlight::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: var(--glass-highlight);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/* Tinted glass (theme-aware) */
|
||||
.glass-tinted {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--about-gradient-from, #fff) 8%, var(--glass-bg-light-subtle)), color-mix(in srgb, var(--about-gradient-to, #fff) 8%, var(--glass-bg-light-subtle)));
|
||||
backdrop-filter: blur(var(--glass-blur-md));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-md));
|
||||
border: 1px solid color-mix(in srgb, var(--about-color, #fff) 20%, var(--glass-border-light));
|
||||
box-shadow: var(--glass-shadow-sm);
|
||||
}
|
||||
|
||||
.glass-tinted-heavy {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--about-gradient-from, #fff) 15%, var(--glass-bg-light-medium)), color-mix(in srgb, var(--about-gradient-to, #fff) 15%, var(--glass-bg-light-medium)));
|
||||
backdrop-filter: blur(var(--glass-blur-lg));
|
||||
-webkit-backdrop-filter: blur(var(--glass-blur-lg));
|
||||
border: 1px solid color-mix(in srgb, var(--about-color, #fff) 30%, var(--glass-border-light));
|
||||
box-shadow: var(--glass-shadow-md), var(--glass-glow-medium);
|
||||
}
|
||||
|
||||
/* ============================================
|
||||
ACCESSIBILITY
|
||||
============================================ */
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.glass-hover,
|
||||
.glass-hover-glow {
|
||||
transition: none;
|
||||
}
|
||||
.glass-hover:hover,
|
||||
.glass-hover-glow:hover {
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-contrast: high) {
|
||||
:root {
|
||||
--glass-border-subtle: rgba(255, 255, 255, 0.2);
|
||||
--glass-border-light: rgba(255, 255, 255, 0.3);
|
||||
--glass-border-medium: rgba(255, 255, 255, 0.4);
|
||||
--glass-border-bright: rgba(255, 255, 255, 0.5);
|
||||
}
|
||||
.glass-subtle,
|
||||
.glass-light,
|
||||
.glass-medium,
|
||||
.glass-heavy,
|
||||
.glass-dark,
|
||||
.glass-tinted,
|
||||
.glass-tinted-heavy {
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
background: rgba(30, 30, 40, 0.95);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue