| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697 |
- <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") }}</span>
- </div>
- <h1 class="font-display text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
- {{ t("portfolio.title") }} <span class="text-gradient">{{ t("portfolio.titleGradient") }}</span>
- </h1>
- <p class="text-muted-foreground max-w-2xl mx-auto text-lg leading-relaxed">
- {{ t("portfolio.description") }}
- </p>
- </div>
- <div v-if="isLoading" class="flex flex-col items-center justify-center py-24 gap-4 opacity-50">
- <RefreshCw class="w-8 h-8 text-primary animate-spin mb-4" />
- <p class="text-sm font-medium animate-pulse">{{ t("portfolio.loading") }}</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 = `${API_BASE_URL}${item.file_path.startsWith('/') ? '' : '/'}${item.file_path}`"
- >
- <img :src="`${API_BASE_URL}${item.file_path.startsWith('/') ? '' : '/'}${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-end">
- <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]">
- <div class="w-20 h-20 bg-primary/10 rounded-full flex items-center justify-center mx-auto mb-6">
- <ImageIcon class="w-10 h-10 text-primary opacity-60" />
- </div>
- <h3 class="text-xl font-bold mb-2">{{ t("portfolio.emptyTitle") }}</h3>
- <p class="text-muted-foreground">{{ t("portfolio.emptyDesc") }}</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, RefreshCw, Image as ImageIcon } from "lucide-vue-next";
- import Header from "@/components/Header.vue";
- import Footer from "@/components/Footer.vue";
- import { getPortfolio, API_BASE_URL } 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>
|