Blog.vue 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154
  1. <template>
  2. <div class="min-h-screen bg-white">
  3. <Header />
  4. <div class="container mx-auto px-4 py-32">
  5. <div class="max-w-6xl mx-auto">
  6. <!-- Header -->
  7. <div class="mb-12">
  8. <h1 class="text-4xl md:text-5xl font-display font-bold text-foreground mb-4">
  9. {{ t("footer.blog") }}
  10. </h1>
  11. <p class="text-lg text-foreground/60">
  12. {{ t("blog.subtitle") }}
  13. </p>
  14. </div>
  15. <!-- Loading State -->
  16. <div v-if="isLoading" class="flex flex-col items-center justify-center py-24 gap-4 opacity-50">
  17. <RefreshCw class="w-8 h-8 text-primary animate-spin mb-4" />
  18. <p class="font-bold tracking-widest uppercase text-[10px]">{{ t("blog.loading") }}</p>
  19. </div>
  20. <template v-else>
  21. <!-- Featured Post (First one) -->
  22. <section v-if="featuredPost" class="mb-12">
  23. <div class="bg-gray-50 rounded-2xl overflow-hidden border border-black/[0.03]">
  24. <div class="md:flex">
  25. <div class="md:w-1/2">
  26. <div class="h-64 md:h-full bg-muted/20 overflow-hidden">
  27. <img v-if="featuredPost.image_url" :src="featuredPost.image_url" class="w-full h-full object-cover" />
  28. </div>
  29. </div>
  30. <div class="md:w-1/2 p-8 md:p-12">
  31. <div class="flex items-center gap-2 mb-4">
  32. <span class="px-3 py-1 bg-primary text-white text-[10px] font-bold rounded-full uppercase tracking-widest">
  33. {{ t("blog.featured") }}
  34. </span>
  35. <span class="text-xs font-bold text-foreground/40 uppercase">
  36. {{ new Date(featuredPost.created_at).toLocaleDateString() }}
  37. </span>
  38. </div>
  39. <h2 class="text-2xl md:text-3xl font-display font-bold text-foreground mb-4">
  40. {{ getLoc(featuredPost, 'title') }}
  41. </h2>
  42. <p class="text-foreground/70 mb-6 line-clamp-3">
  43. {{ getLoc(featuredPost, 'excerpt') }}
  44. </p>
  45. <router-link
  46. :to="`/blog/${featuredPost.slug}`"
  47. class="inline-flex items-center text-primary font-bold hover:text-primary/80 transition-colors"
  48. >
  49. {{ t("blog.readMore") }}
  50. <ArrowRight class="w-4 h-4 ml-2" />
  51. </router-link>
  52. </div>
  53. </div>
  54. </div>
  55. </section>
  56. <!-- Blog Posts Grid -->
  57. <section v-if="remainingPosts.length > 0">
  58. <h2 class="text-xl font-display font-bold text-foreground mb-8 uppercase tracking-widest opacity-40">
  59. {{ t("blog.latestPosts") }}
  60. </h2>
  61. <div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
  62. <article v-for="post in remainingPosts" :key="post.id" class="flex flex-col border border-black/[0.05] rounded-2xl overflow-hidden hover:border-primary/30 transition-all duration-300 group">
  63. <div class="h-48 bg-muted/20 overflow-hidden">
  64. <img v-if="post.image_url" :src="post.image_url" class="w-full h-full object-cover group-hover:scale-105 transition-transform duration-500" />
  65. </div>
  66. <div class="p-6 flex-1 flex flex-col">
  67. <div class="flex items-center justify-between mb-4">
  68. <span class="text-[10px] font-bold text-foreground/30 uppercase">
  69. {{ new Date(post.created_at).toLocaleDateString() }}
  70. </span>
  71. <span class="px-2 py-0.5 bg-gray-50 text-foreground/40 text-[9px] font-bold rounded-md border border-black/[0.03] uppercase tracking-wider">
  72. {{ post.category }}
  73. </span>
  74. </div>
  75. <h3 class="text-lg font-display font-bold text-foreground mb-3 line-clamp-2">
  76. {{ getLoc(post, 'title') }}
  77. </h3>
  78. <p class="text-foreground/60 text-sm mb-6 line-clamp-3 flex-1">
  79. {{ getLoc(post, 'excerpt') }}
  80. </p>
  81. <router-link
  82. :to="`/blog/${post.slug}`"
  83. class="inline-flex items-center text-primary font-bold hover:text-primary-foreground hover:bg-primary px-4 py-2 rounded-xl transition-all self-start text-xs border border-primary/20"
  84. >
  85. {{ t("blog.readMore") }}
  86. </router-link>
  87. </div>
  88. </article>
  89. </div>
  90. </section>
  91. </template>
  92. <!-- Newsletter CTA -->
  93. <section class="mt-20 bg-primary/[0.02] border border-primary/10 rounded-3xl p-8 md:p-12 text-center">
  94. <h2 class="text-2xl font-display font-bold text-foreground mb-4">
  95. {{ t("blog.newsletter.title") }}
  96. </h2>
  97. <p class="text-foreground/70 mb-8 max-w-xl mx-auto text-sm leading-relaxed">
  98. {{ t("blog.newsletter.content") }}
  99. </p>
  100. <div class="max-w-md mx-auto">
  101. <div class="flex flex-col sm:flex-row gap-2">
  102. <input
  103. type="email"
  104. :placeholder="t('blog.newsletter.placeholder')"
  105. class="flex-1 px-5 py-3 bg-white border border-black/[0.05] rounded-2xl focus:outline-none focus:ring-2 focus:ring-primary/20 transition-all text-sm"
  106. />
  107. <Button variant="hero" class="px-8 whitespace-nowrap">
  108. {{ t("blog.newsletter.subscribe") }}
  109. </Button>
  110. </div>
  111. </div>
  112. </section>
  113. </div>
  114. </div>
  115. <Footer />
  116. </div>
  117. </template>
  118. <script setup lang="ts">
  119. import { ref, onMounted, computed } from "vue";
  120. import { useI18n } from "vue-i18n";
  121. import { ArrowRight, RefreshCw } from "lucide-vue-next";
  122. import Header from "@/components/Header.vue";
  123. import Footer from "@/components/Footer.vue";
  124. import Button from "@/components/ui/button.vue";
  125. import { getBlogPosts } from "@/lib/api";
  126. const { t, locale } = useI18n();
  127. const posts = ref<any[]>([]);
  128. const isLoading = ref(true);
  129. const featuredPost = computed(() => posts.value[0]);
  130. const remainingPosts = computed(() => posts.value.slice(1));
  131. function getLoc(item: any, field: string) {
  132. const currentLocale = locale.value;
  133. // Try current locale, fallback to EN
  134. return item[`${field}_${currentLocale}`] || item[`${field}_en`] || "";
  135. }
  136. onMounted(async () => {
  137. try {
  138. posts.value = await getBlogPosts();
  139. } catch (e) {
  140. console.error("Failed to fetch posts", e);
  141. } finally {
  142. isLoading.value = false;
  143. }
  144. });
  145. </script>