| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596 |
- <template>
- <div class="min-h-screen bg-background">
- <Header />
- <main class="container mx-auto px-4 pt-32 pb-24">
- <div class="text-center mb-16">
- <div v-motion :initial="{ opacity: 0, scale: 0.9 }" :enter="{ opacity: 1, scale: 1 }"
- class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary mb-6">
- <Sparkles class="w-4 h-4" />
- <span class="text-xs font-display font-medium tracking-wider uppercase">{{ t("nav.portfolio") || "Showcase" }}</span>
- </div>
- <h1 class="font-display text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
- {{ t("portfolio.title") || "Public" }} <span class="text-gradient">{{ t("portfolio.titleGradient") || "Portfolio" }}</span>
- </h1>
- <p class="text-muted-foreground max-w-2xl mx-auto text-lg leading-relaxed">
- {{ t("portfolio.description") || "Explore our successful 3D printing projects." }}
- </p>
- </div>
- <div v-if="isLoading" class="flex flex-col items-center justify-center py-24 gap-4 opacity-50">
- <Loader2 class="w-10 h-10 animate-spin text-primary" />
- <p class="text-sm font-medium animate-pulse">Loading gallery...</p>
- </div>
- <div v-else-if="items.length > 0" class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-6">
- <div
- v-for="(item, idx) in items"
- :key="item.id"
- v-motion
- :initial="{ opacity: 0, y: 20 }"
- :enter="{ opacity: 1, y: 0, transition: { delay: idx * 50 } }"
- class="group relative aspect-square overflow-hidden rounded-3xl border border-border/50 bg-card/40 backdrop-blur-sm cursor-pointer hover:border-primary/50 transition-all duration-500"
- @click="selectedImage = `http://localhost:8000/${item.file_path}`"
- >
- <img :src="`http://localhost:8000/${item.file_path}`" :alt="item.material_name"
- class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" />
- <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-black/20 to-transparent opacity-0 group-hover:opacity-100 transition-all duration-500 flex flex-col justify-end p-6">
- <span class="text-[10px] font-bold uppercase tracking-widest text-primary mb-1">{{ item.material_name }}</span>
- <div class="flex items-center justify-between">
- <p class="text-white font-bold">Order #{{ item.order_id }}</p>
- <ExternalLink class="w-4 h-4 text-white/70" />
- </div>
- </div>
- </div>
- </div>
- <div v-else class="text-center py-32 bg-card/20 border border-dashed border-border/50 rounded-[40px]">
- <Camera class="w-12 h-12 text-muted-foreground mb-4 mx-auto opacity-20" />
- <h3 class="text-xl font-bold mb-2">Our gallery is growing</h3>
- <p class="text-muted-foreground">Check back soon for more amazing prints!</p>
- </div>
- </main>
- <!-- Lightbox -->
- <Teleport to="body">
- <Transition enter-active-class="transition duration-200" enter-from-class="opacity-0" enter-to-class="opacity-100"
- leave-active-class="transition duration-150" leave-from-class="opacity-100" leave-to-class="opacity-0">
- <div v-if="selectedImage"
- class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-background/95 backdrop-blur-md"
- @click="selectedImage = null">
- <div
- v-motion :initial="{ scale: 0.9, opacity: 0 }" :enter="{ scale: 1, opacity: 1 }"
- class="relative max-w-5xl w-full max-h-[90vh] overflow-hidden rounded-2xl shadow-glow"
- @click.stop>
- <img :src="selectedImage" class="w-full h-full object-contain" />
- <button @click="selectedImage = null"
- class="absolute top-4 right-4 w-10 h-10 bg-black/50 hover:bg-black/80 text-white rounded-full flex items-center justify-center transition-colors">
- ✕
- </button>
- </div>
- </div>
- </Transition>
- </Teleport>
- <Footer />
- </div>
- </template>
- <script setup lang="ts">
- import { ref, onMounted } from "vue";
- import { useI18n } from "vue-i18n";
- import { Sparkles, Loader2, Camera, ExternalLink } from "lucide-vue-next";
- import Header from "@/components/Header.vue";
- import Footer from "@/components/Footer.vue";
- import { getPortfolio } from "@/lib/api";
- const { t } = useI18n();
- const items = ref<any[]>([]);
- const isLoading = ref(true);
- const selectedImage = ref<string | null>(null);
- onMounted(async () => {
- try { items.value = await getPortfolio(); }
- catch (e) { console.error("Failed to load portfolio:", e); }
- finally { isLoading.value = false; }
- });
- </script>
|