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/vue@3.3.4/dist/vue.global.prod.js"></script> | |
| <link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/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> |