Spaces:
Sleeping
Sleeping
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
<title>Electronics Store</title> | |
<script src="https://unpkg.com/[email protected]/dist/vue.global.prod.js"></script> | |
<link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/tailwind.min.css" rel="stylesheet"> | |
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet"> | |
<style> | |
body { | |
font-family: 'Inter', sans-serif; | |
} | |
.fade-enter-active, .fade-leave-active { | |
transition: opacity 0.3s ease; | |
} | |
.fade-enter-from, .fade-leave-to { | |
opacity: 0; | |
} | |
.product-card { | |
transform: translateY(0); | |
transition: all 0.3s ease; | |
} | |
.product-card:hover { | |
transform: translateY(-5px); | |
} | |
.stock-badge { | |
transition: all 0.3s ease; | |
} | |
</style> | |
</head> | |
<body class="bg-gray-50"> | |
<div id="app" class="min-h-screen"> | |
<!-- Header --> | |
<header class="bg-white shadow-sm"> | |
<div class="container mx-auto px-4 py-6"> | |
<h1 class="text-4xl font-bold text-gray-900">Electronics Store</h1> | |
</div> | |
</header> | |
<main class="container mx-auto px-4 py-8"> | |
<!-- Filters Section --> | |
<div class="bg-white rounded-xl shadow-sm p-6 mb-8"> | |
<div class="flex flex-col md:flex-row gap-4 items-center justify-between"> | |
<div class="flex gap-4 w-full md:w-auto"> | |
<select v-model="selectedCategory" | |
class="w-full md:w-48 p-2.5 border border-gray-200 rounded-lg bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="">All Categories</option> | |
<option v-for="category in categories" :key="category" :value="category"> | |
{{ category }} | |
</option> | |
</select> | |
<select v-model="sortBy" | |
class="w-full md:w-48 p-2.5 border border-gray-200 rounded-lg bg-gray-50 focus:ring-2 focus:ring-blue-500 focus:border-blue-500"> | |
<option value="price-asc">Price: Low to High</option> | |
<option value="price-desc">Price: High to Low</option> | |
<option value="rating">Top Rated</option> | |
</select> | |
</div> | |
<div class="text-sm text-gray-500"> | |
{{ filteredProducts.length }} products found | |
</div> | |
</div> | |
</div> | |
<!-- Products Grid --> | |
<transition-group name="fade" tag="div" | |
class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6"> | |
<div v-for="product in filteredProducts" | |
:key="product.id" | |
class="product-card bg-white rounded-xl shadow-sm overflow-hidden"> | |
<div class="p-6"> | |
<!-- Product Category Badge --> | |
<div class="inline-block px-3 py-1 rounded-full text-xs font-medium bg-blue-50 text-blue-600 mb-4"> | |
{{ product.category }} | |
</div> | |
<!-- Product Name --> | |
<h2 class="text-xl font-semibold text-gray-900 mb-4">{{ product.name }}</h2> | |
<!-- Specifications --> | |
<div class="space-y-3 mb-6"> | |
<div v-for="(value, key) in product.specs" | |
:key="key" | |
class="flex justify-between text-sm"> | |
<span class="text-gray-500">{{ key }}</span> | |
<span class="text-gray-900 font-medium">{{ value }}</span> | |
</div> | |
</div> | |
<!-- Price and Rating --> | |
<div class="flex items-center justify-between pt-4 border-t border-gray-100"> | |
<div class="text-2xl font-bold text-gray-900"> | |
${{ product.price.toLocaleString() }} | |
</div> | |
<div class="flex items-center gap-1"> | |
<span class="text-yellow-400 text-lg">★</span> | |
<span class="font-medium">{{ product.rating }}</span> | |
</div> | |
</div> | |
<!-- Stock Status --> | |
<div class="mt-4"> | |
<span class="stock-badge px-3 py-1 rounded-full text-sm font-medium" | |
:class="{ | |
'bg-red-50 text-red-600': product.stock < 10, | |
'bg-green-50 text-green-600': product.stock >= 10 | |
}"> | |
{{ product.stock }} in stock | |
</span> | |
</div> | |
</div> | |
</div> | |
</transition-group> | |
<!-- Empty State --> | |
<div v-if="filteredProducts.length === 0" | |
class="text-center py-12"> | |
<p class="text-gray-500">No products found matching your criteria</p> | |
</div> | |
</main> | |
</div> | |
<script> | |
const { createApp, ref, computed } = Vue | |
createApp({ | |
setup() { | |
const products = ref([]) | |
const selectedCategory = ref('') | |
const sortBy = ref('price-asc') | |
// Fetch products from API | |
fetch('/api/electronics/products') | |
.then(response => response.json()) | |
.then(data => { | |
products.value = data.products || [] | |
}) | |
.catch(error => { | |
console.error('Error fetching products:', error) | |
}) | |
const categories = computed(() => { | |
if (!products.value?.length) return [] | |
return [...new Set(products.value.map(p => p.category))] | |
}) | |
const filteredProducts = computed(() => { | |
if (!products.value?.length) return [] | |
let filtered = [...products.value] | |
if (selectedCategory.value) { | |
filtered = filtered.filter(p => p.category === selectedCategory.value) | |
} | |
switch (sortBy.value) { | |
case 'price-asc': | |
filtered.sort((a, b) => a.price - b.price) | |
break | |
case 'price-desc': | |
filtered.sort((a, b) => b.price - a.price) | |
break | |
case 'rating': | |
filtered.sort((a, b) => b.rating - a.rating) | |
break | |
} | |
return filtered | |
}) | |
return { | |
products, | |
categories, | |
selectedCategory, | |
sortBy, | |
filteredProducts | |
} | |
} | |
}).mount('#app') | |
</script> | |
</body> | |
</html> |