Portfolio.vue 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  1. <template>
  2. <div class="min-h-screen bg-background">
  3. <Header />
  4. <main class="container mx-auto px-4 pt-32 pb-24">
  5. <div class="text-center mb-16">
  6. <div v-motion :initial="{ opacity: 0, scale: 0.9 }" :enter="{ opacity: 1, scale: 1 }"
  7. class="inline-flex items-center gap-2 px-3 py-1 rounded-full bg-primary/10 border border-primary/20 text-primary mb-6">
  8. <Sparkles class="w-4 h-4" />
  9. <span class="text-xs font-display font-medium tracking-wider uppercase">{{ t("nav.portfolio") || "Showcase" }}</span>
  10. </div>
  11. <h1 class="font-display text-4xl sm:text-5xl lg:text-6xl font-bold mb-6">
  12. {{ t("portfolio.title") || "Public" }} <span class="text-gradient">{{ t("portfolio.titleGradient") || "Portfolio" }}</span>
  13. </h1>
  14. <p class="text-muted-foreground max-w-2xl mx-auto text-lg leading-relaxed">
  15. {{ t("portfolio.description") || "Explore our successful 3D printing projects." }}
  16. </p>
  17. </div>
  18. <div v-if="isLoading" class="flex flex-col items-center justify-center py-24 gap-4 opacity-50">
  19. <Loader2 class="w-10 h-10 animate-spin text-primary" />
  20. <p class="text-sm font-medium animate-pulse">Loading gallery...</p>
  21. </div>
  22. <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">
  23. <div
  24. v-for="(item, idx) in items"
  25. :key="item.id"
  26. v-motion
  27. :initial="{ opacity: 0, y: 20 }"
  28. :enter="{ opacity: 1, y: 0, transition: { delay: idx * 50 } }"
  29. 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"
  30. @click="selectedImage = `http://localhost:8000/${item.file_path}`"
  31. >
  32. <img :src="`http://localhost:8000/${item.file_path}`" :alt="item.material_name"
  33. class="w-full h-full object-cover transition-transform duration-700 group-hover:scale-110" />
  34. <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">
  35. <span class="text-[10px] font-bold uppercase tracking-widest text-primary mb-1">{{ item.material_name }}</span>
  36. <div class="flex items-center justify-between">
  37. <p class="text-white font-bold">Order #{{ item.order_id }}</p>
  38. <ExternalLink class="w-4 h-4 text-white/70" />
  39. </div>
  40. </div>
  41. </div>
  42. </div>
  43. <div v-else class="text-center py-32 bg-card/20 border border-dashed border-border/50 rounded-[40px]">
  44. <Camera class="w-12 h-12 text-muted-foreground mb-4 mx-auto opacity-20" />
  45. <h3 class="text-xl font-bold mb-2">Our gallery is growing</h3>
  46. <p class="text-muted-foreground">Check back soon for more amazing prints!</p>
  47. </div>
  48. </main>
  49. <!-- Lightbox -->
  50. <Teleport to="body">
  51. <Transition enter-active-class="transition duration-200" enter-from-class="opacity-0" enter-to-class="opacity-100"
  52. leave-active-class="transition duration-150" leave-from-class="opacity-100" leave-to-class="opacity-0">
  53. <div v-if="selectedImage"
  54. class="fixed inset-0 z-[200] flex items-center justify-center p-4 bg-background/95 backdrop-blur-md"
  55. @click="selectedImage = null">
  56. <div
  57. v-motion :initial="{ scale: 0.9, opacity: 0 }" :enter="{ scale: 1, opacity: 1 }"
  58. class="relative max-w-5xl w-full max-h-[90vh] overflow-hidden rounded-2xl shadow-glow"
  59. @click.stop>
  60. <img :src="selectedImage" class="w-full h-full object-contain" />
  61. <button @click="selectedImage = null"
  62. 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">
  63. </button>
  64. </div>
  65. </div>
  66. </Transition>
  67. </Teleport>
  68. <Footer />
  69. </div>
  70. </template>
  71. <script setup lang="ts">
  72. import { ref, onMounted } from "vue";
  73. import { useI18n } from "vue-i18n";
  74. import { Sparkles, Loader2, Camera, ExternalLink } from "lucide-vue-next";
  75. import Header from "@/components/Header.vue";
  76. import Footer from "@/components/Footer.vue";
  77. import { getPortfolio } from "@/lib/api";
  78. const { t } = useI18n();
  79. const items = ref<any[]>([]);
  80. const isLoading = ref(true);
  81. const selectedImage = ref<string | null>(null);
  82. onMounted(async () => {
  83. try { items.value = await getPortfolio(); }
  84. catch (e) { console.error("Failed to load portfolio:", e); }
  85. finally { isLoading.value = false; }
  86. });
  87. </script>