feat: migrate landing page from Next.js to Astro
Replaces the heavy Next.js SSR app with Astro static site: - Zero JS shipped for static pages (hero, community, CTA, footer) - Server-side data fetching in Astro frontmatter (events, hackathon, testimonials) - Only 4 React islands for interactive parts (mobile menu, roadmap voting, testimonials home, feature request) - Google Fonts via <link> instead of next/font - Nix build changed from Node.js systemd service to static nginx (mkStaticAppModule) - npmDepsHash needs update (set to fake hash, CI will provide correct one) Performance gains: - No Node.js server process needed (was using ~10MB RAM) - No Next.js runtime JS (~200KB+ saved) - No framer-motion bundle (~130KB saved) - Static HTML served directly by nginx Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
0ade3c3e50
commit
95bf09c2fa
@@ -0,0 +1,56 @@
|
||||
---
|
||||
---
|
||||
|
||||
<section class="w-full py-20 md:py-32 bg-gradient-to-br from-primary-500 to-primary-600 relative overflow-hidden">
|
||||
<div
|
||||
class="absolute inset-0 opacity-10"
|
||||
style="background-image: url("data:image/svg+xml,%3Csvg width='52' height='26' viewBox='0 0 52 26' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Cg fill='%23ffffff' fill-opacity='0.4'%3E%3Cpath d='M10 10c0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6h2c0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4 3.314 0 6 2.686 6 6 0 2.21 1.79 4 4 4v2c-3.314 0-6-2.686-6-6 0-2.21-1.79-4-4-4-3.314 0-6-2.686-6-6zm25.464-1.95l8.486 8.486-1.414 1.414-8.486-8.486 1.414-1.414z' /%3E%3C/g%3E%3C/g%3E%3C/svg%3E");"
|
||||
></div>
|
||||
|
||||
<div class="container relative">
|
||||
<div class="flex flex-col lg:flex-row items-center justify-between gap-12">
|
||||
<div class="flex-1 text-center lg:text-left space-y-8">
|
||||
<div>
|
||||
<h2 class="text-4xl md:text-5xl font-bold text-white leading-tight">
|
||||
<span class="block">LET'S GO</span>
|
||||
<span class="block text-5xl md:text-6xl mt-2">
|
||||
<span class="text-yellow-300">SAAT</span><span class="text-white">NYA</span>
|
||||
</span>
|
||||
<span class="block mt-2">KAMU JOIN!</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<p class="text-lg md:text-xl text-white/90 max-w-2xl mx-auto lg:mx-0">
|
||||
Jadilah bagian dari komunitas developer terbesar di Indonesia.
|
||||
Tingkatkan skill, perluas jaringan, dan raih kesempatan karir
|
||||
bersama kami!
|
||||
</p>
|
||||
|
||||
<div class="flex justify-center lg:justify-start">
|
||||
<a
|
||||
href="#community"
|
||||
class="flex items-center gap-4 px-8 py-4 bg-yellow-300 hover:bg-yellow-400 text-gray-900 rounded-full text-lg font-semibold transition-all hover:gap-6 group"
|
||||
>
|
||||
<span>Join Sekarang</span>
|
||||
<svg fill="currentColor" viewBox="0 0 448 512" class="w-5 h-5 transition-all group-hover:rotate-45"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 max-w-xl">
|
||||
<div class="relative p-8">
|
||||
<div class="absolute inset-0 bg-white/10 rounded-3xl transform rotate-6"></div>
|
||||
<div class="relative bg-white/5 rounded-3xl p-8 backdrop-blur-lg border border-white/10">
|
||||
<div class="flex flex-col items-center gap-6 text-white">
|
||||
<img src="/logo.webp" alt="IMPHNEN" class="w-[240px] h-auto" />
|
||||
<div class="text-center space-y-2">
|
||||
<h3 class="text-2xl font-bold">250.000+</h3>
|
||||
<p class="text-lg">Programmer Sudah Bergabung</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,66 @@
|
||||
---
|
||||
import socials from '../data/socials.json';
|
||||
|
||||
const ICON_SVG: Record<string, string> = {
|
||||
FaFacebook: '<svg fill="currentColor" viewBox="0 0 320 512" class="w-8 h-8"><path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4.4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"/></svg>',
|
||||
FaDiscord: '<svg fill="currentColor" viewBox="0 0 640 512" class="w-8 h-8"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>',
|
||||
FaGithub: '<svg fill="currentColor" viewBox="0 0 496 512" class="w-8 h-8"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8z"/></svg>',
|
||||
FaInstagram: '<svg fill="currentColor" viewBox="0 0 448 512" class="w-8 h-8"><path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/></svg>',
|
||||
FaTiktok: '<svg fill="currentColor" viewBox="0 0 448 512" class="w-8 h-8"><path d="M448,209.91a210.06,210.06,0,0,1-122.77-39.25V349.38A162.55,162.55,0,1,1,185,188.31V278.2a74.62,74.62,0,1,0,52.23,71.18V0l88,0a121.18,121.18,0,0,0,1.86,22.17h0A122.18,122.18,0,0,0,381,102.39a121.43,121.43,0,0,0,67,20.14Z"/></svg>',
|
||||
FaLinkedin: '<svg fill="currentColor" viewBox="0 0 448 512" class="w-8 h-8"><path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.83-48.3 93.97 0 111.28 61.9 111.28 142.3V448z"/></svg>',
|
||||
};
|
||||
|
||||
const COLOR_MAP: Record<string, string> = {
|
||||
FaFacebook: 'text-[#1877F2]',
|
||||
FaDiscord: 'text-[#5865F2]',
|
||||
FaGithub: 'text-[#000000]',
|
||||
FaInstagram: 'text-[#E4405F]',
|
||||
FaTiktok: 'text-[#000000]',
|
||||
FaLinkedin: 'text-[#0A66C2]',
|
||||
};
|
||||
---
|
||||
|
||||
<section id="community" class="w-full py-20 md:py-28">
|
||||
<div class="container">
|
||||
<div class="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<h2 class="text-3xl font-semibold tracking-tight md:text-4xl">
|
||||
Bergabung dengan Komunitas Kami di
|
||||
<span class="block mt-2 text-primary-500">Berbagai Platform</span>
|
||||
</h2>
|
||||
<p class="max-w-[600px] mx-auto text-gray-600 md:text-lg/relaxed mt-4">
|
||||
Terhubung dengan sesama developer di komunitas kami
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 grid-cols-1 md:grid-cols-2 lg:grid-cols-3">
|
||||
{socials.map((community: any, index: number) => {
|
||||
const iconColor = COLOR_MAP[community.icon] || 'text-primary-500';
|
||||
const iconSvg = ICON_SVG[community.icon] || '';
|
||||
|
||||
return (
|
||||
<div
|
||||
class="group relative p-8 bg-white border border-gray-200 rounded-xl hover:border-gray-300 transition-all duration-300 animate-[fadeInUp_0.4s_ease-out]"
|
||||
style={`animation-delay: ${index * 80}ms; animation-fill-mode: both;`}
|
||||
>
|
||||
<div class="flex flex-col items-start gap-5">
|
||||
<div class="flex items-center gap-4">
|
||||
<div class={iconColor} set:html={iconSvg} />
|
||||
<h3 class="text-xl font-semibold text-gray-900">{community.name}</h3>
|
||||
</div>
|
||||
<p class="text-gray-600 text-sm/relaxed">{community.description}</p>
|
||||
<a
|
||||
href={community.link}
|
||||
class="inline-flex items-center gap-2 mt-2 text-sm font-medium transition-colors"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<span>Jelajahi Komunitas</span>
|
||||
<svg fill="currentColor" viewBox="0 0 448 512" class="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1"><path d="M438.6 278.6c12.5-12.5 12.5-32.8 0-45.3l-160-160c-12.5-12.5-32.8-12.5-45.3 0s-12.5 32.8 0 45.3L338.8 224 32 224c-17.7 0-32 14.3-32 32s14.3 32 32 32l306.7 0L233.4 393.4c-12.5 12.5-12.5 32.8 0 45.3s32.8 12.5 45.3 0l160-160z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
import navigations from '../data/navigations.json';
|
||||
import socials from '../data/socials.json';
|
||||
|
||||
const year = new Date().getFullYear();
|
||||
---
|
||||
|
||||
<footer class="w-full border-t bg-background py-12 md:py-16">
|
||||
<div class="container px-4 md:px-6">
|
||||
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-4">
|
||||
<div class="space-y-4">
|
||||
<div class="flex items-center gap-2">
|
||||
<img src="/logo.webp" alt="IMPHNEN" class="w-28 h-auto" />
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Ingin Menjadi Programmer Handal Namun Enggan Ngoding
|
||||
</p>
|
||||
<div class="flex space-x-4">
|
||||
<a href="https://fb.com/groups/programmerhandal" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 320 512"><path d="M80 299.3V512H196V299.3h86.5l18-97.8H196V166.9c0-51.7 20.3-71.5 72.7-71.5c16.3 0 29.4.4 37 1.2V7.9C291.4 4 256.4 0 236.2 0C129.3 0 80 50.5 80 159.4v42.1H14v97.8H80z"/></svg>
|
||||
</a>
|
||||
<a href="https://discord.gg/imphnen" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 640 512"><path d="M524.531,69.836a1.5,1.5,0,0,0-.764-.7A485.065,485.065,0,0,0,404.081,32.03a1.816,1.816,0,0,0-1.923.91,337.461,337.461,0,0,0-14.9,30.6,447.848,447.848,0,0,0-134.426,0,309.541,309.541,0,0,0-15.135-30.6,1.89,1.89,0,0,0-1.924-.91A483.689,483.689,0,0,0,116.085,69.137a1.712,1.712,0,0,0-.788.676C39.068,183.651,18.186,294.69,28.43,404.354a2.016,2.016,0,0,0,.765,1.375A487.666,487.666,0,0,0,176.02,479.918a1.9,1.9,0,0,0,2.063-.676A348.2,348.2,0,0,0,208.12,430.4a1.86,1.86,0,0,0-1.019-2.588,321.173,321.173,0,0,1-45.868-21.853,1.885,1.885,0,0,1-.185-3.126c3.082-2.309,6.166-4.711,9.109-7.137a1.819,1.819,0,0,1,1.9-.256c96.229,43.917,200.41,43.917,295.5,0a1.812,1.812,0,0,1,1.924.233c2.944,2.426,6.027,4.851,9.132,7.16a1.884,1.884,0,0,1-.162,3.126,301.407,301.407,0,0,1-45.89,21.83,1.875,1.875,0,0,0-1,2.611,391.055,391.055,0,0,0,30.014,48.815,1.864,1.864,0,0,0,2.063.7A486.048,486.048,0,0,0,610.7,405.729a1.882,1.882,0,0,0,.765-1.352C623.729,277.594,590.933,167.465,524.531,69.836ZM222.491,337.58c-28.972,0-52.844-26.587-52.844-59.239S193.056,219.1,222.491,219.1c29.665,0,53.306,26.82,52.843,59.239C275.334,310.993,251.924,337.58,222.491,337.58Zm195.38,0c-28.971,0-52.843-26.587-52.843-59.239S388.437,219.1,417.871,219.1c29.667,0,53.307,26.82,52.844,59.239C470.715,310.993,447.538,337.58,417.871,337.58Z"/></svg>
|
||||
</a>
|
||||
<a href="https://github.com/IMPHNEN/" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 496 512"><path d="M165.9 397.4c0 2-2.3 3.6-5.2 3.6-3.3.3-5.6-1.3-5.6-3.6 0-2 2.3-3.6 5.2-3.6 3-.3 5.6 1.3 5.6 3.6zm-31.1-4.5c-.7 2 1.3 4.3 4.3 4.9 2.6 1 5.6 0 6.2-2s-1.3-4.3-4.3-5.2c-2.6-.7-5.5.3-6.2 2.3zm44.2-1.7c-2.9.7-4.9 2.6-4.6 4.9.3 2 2.9 3.3 5.9 2.6 2.9-.7 4.9-2.6 4.6-4.6-.3-1.9-3-3.2-5.9-2.9zM244.8 8C106.1 8 0 113.3 0 252c0 110.9 69.8 205.8 169.5 239.2 12.8 2.3 17.3-5.6 17.3-12.1 0-6.2-.3-40.4-.3-61.4 0 0-70 15-84.7-29.8 0 0-11.4-29.1-27.8-36.6 0 0-22.9-15.7 1.6-15.4 0 0 24.9 2 38.6 25.8 21.9 38.6 58.6 27.5 72.9 20.9 2.3-16 8.8-27.1 16-33.7-55.9-6.2-112.3-14.3-112.3-110.5 0-27.5 7.6-41.3 23.6-58.9-2.6-6.5-11.1-33.3 2.6-67.9 20.9-6.5 69 27 69 27 20-5.6 41.5-8.5 62.8-8.5s42.8 2.9 62.8 8.5c0 0 48.1-33.6 69-27 13.7 34.7 5.2 61.4 2.6 67.9 16 17.7 25.8 31.5 25.8 58.9 0 96.5-58.9 104.2-114.8 110.5 9.2 7.9 17 22.9 17 46.4 0 33.7-.3 75.4-.3 83.6 0 6.5 4.6 14.4 17.3 12.1C428.2 457.8 496 362.9 496 252 496 113.3 383.5 8 244.8 8zM97.2 352.9c-1.3 1-1 3.3.7 5.2 1.6 1.6 3.9 2.3 5.2 1 1.3-1 1-3.3-.7-5.2-1.6-1.6-3.9-2.3-5.2-1zm-10.8-8.1c-.7 1.3.3 2.9 2.3 3.9 1.6 1 3.6.7 4.3-.7.7-1.3-.3-2.9-2.3-3.9-2-.6-3.6-.3-4.3.7zm32.4 35.6c-1.6 1.3-1 4.3 1.3 6.2 2.3 2.3 5.2 2.6 6.5 1 1.3-1.3.7-4.3-1.3-6.2-2.2-2.3-5.2-2.6-6.5-1zm-11.4-14.7c-1.6 1-1.6 3.6 0 5.9 1.6 2.3 4.3 3.3 5.6 2.3 1.6-1.3 1.6-3.9 0-6.2-1.4-2.3-4-3.3-5.6-2z"/></svg>
|
||||
</a>
|
||||
<a href="https://www.instagram.com/imphnen.dev" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 448 512"><path d="M224.1 141c-63.6 0-114.9 51.3-114.9 114.9s51.3 114.9 114.9 114.9S339 319.5 339 255.9 287.7 141 224.1 141zm0 189.6c-41.1 0-74.7-33.5-74.7-74.7s33.5-74.7 74.7-74.7 74.7 33.5 74.7 74.7-33.6 74.7-74.7 74.7zm146.4-194.3c0 14.9-12 26.8-26.8 26.8-14.9 0-26.8-12-26.8-26.8s12-26.8 26.8-26.8 26.8 12 26.8 26.8zm76.1 27.2c-1.7-35.9-9.9-67.7-36.2-93.9-26.2-26.2-58-34.4-93.9-36.2-37-2.1-147.9-2.1-184.9 0-35.8 1.7-67.6 9.9-93.9 36.1s-34.4 58-36.2 93.9c-2.1 37-2.1 147.9 0 184.9 1.7 35.9 9.9 67.7 36.2 93.9s58 34.4 93.9 36.2c37 2.1 147.9 2.1 184.9 0 35.9-1.7 67.7-9.9 93.9-36.2 26.2-26.2 34.4-58 36.2-93.9 2.1-37 2.1-147.8 0-184.8zM398.8 388c-7.8 19.6-22.9 34.7-42.6 42.6-29.5 11.7-99.5 9-132.1 9s-102.7 2.6-132.1-9c-19.6-7.8-34.7-22.9-42.6-42.6-11.7-29.5-9-99.5-9-132.1s-2.6-102.7 9-132.1c7.8-19.6 22.9-34.7 42.6-42.6 29.5-11.7 99.5-9 132.1-9s102.7-2.6 132.1 9c19.6 7.8 34.7 22.9 42.6 42.6 11.7 29.5 9 99.5 9 132.1s2.7 102.7-9 132.1z"/></svg>
|
||||
</a>
|
||||
<a href="https://www.tiktok.com/@imphnen" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 448 512"><path d="M448,209.91a210.06,210.06,0,0,1-122.77-39.25V349.38A162.55,162.55,0,1,1,185,188.31V278.2a74.62,74.62,0,1,0,52.23,71.18V0l88,0a121.18,121.18,0,0,0,1.86,22.17h0A122.18,122.18,0,0,0,381,102.39a121.43,121.43,0,0,0,67,20.14Z"/></svg>
|
||||
</a>
|
||||
<a href="https://www.linkedin.com/company/imphnen" class="text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
<svg class="size-6" fill="currentColor" viewBox="0 0 448 512"><path d="M100.28 448H7.4V148.9h92.88zM53.79 108.1C24.09 108.1 0 83.5 0 53.8a53.79 53.79 0 0 1 107.58 0c0 29.7-24.1 54.3-53.79 54.3zM447.9 448h-92.68V302.4c0-34.7-.7-79.2-48.29-79.2-48.29 0-55.69 37.7-55.69 76.7V448h-92.78V148.9h89.08v40.8h1.3c12.4-23.5 42.69-48.3 87.83-48.3 93.97 0 111.28 61.9 111.28 142.3V448z"/></svg>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-bold">Halaman</h3>
|
||||
<ul class="space-y-2">
|
||||
{navigations.map(({ link, title }: { link: string; title: string }) => (
|
||||
<li>
|
||||
<a href={link} class="text-sm text-muted-foreground hover:text-foreground">
|
||||
{title}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-bold">Link</h3>
|
||||
<ul class="space-y-2">
|
||||
{socials.map(({ link, name }: { link: string; name: string }) => (
|
||||
<li>
|
||||
<a href={link} class="text-sm text-muted-foreground hover:text-foreground" target="_blank" rel="noopener noreferrer">
|
||||
{name}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div class="space-y-4">
|
||||
<h3 class="text-lg font-bold">Partners</h3>
|
||||
<ul class="space-y-2"></ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-8 border-t pt-8 text-center space-y-2">
|
||||
<p class="text-xs text-muted-foreground">
|
||||
© {year} IMPHNEN - Ingin Menjadi Programmer Handal, Namun Enggan Ngoding. All rights reserved.
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">
|
||||
Powered by{' '}
|
||||
<a href="https://ancikri.com" target="_blank" rel="noopener noreferrer" class="font-medium text-foreground hover:underline">
|
||||
Ancikri
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
@@ -0,0 +1,35 @@
|
||||
---
|
||||
import navigations from '../data/navigations.json';
|
||||
import MobileMenu from './MobileMenu';
|
||||
|
||||
const currentPath = Astro.url.pathname;
|
||||
---
|
||||
|
||||
<header class="sticky top-0 w-full z-50 bg-background/70 backdrop-blur-sm">
|
||||
<div class="container flex h-20 items-center justify-between">
|
||||
<a href="/" class="relative overflow-hidden rounded z-50" aria-label="Home">
|
||||
<img src="/logo.webp" alt="IMPHNEN" class="w-24 md:w-28 h-auto" />
|
||||
</a>
|
||||
|
||||
<nav class="hidden md:flex items-center gap-8">
|
||||
{navigations.map(({ link, title }: { link: string; title: string }) => (
|
||||
<a
|
||||
href={link}
|
||||
class:list={[
|
||||
'text-sm font-medium relative group px-1 py-2',
|
||||
currentPath === link ? 'text-primary' : 'text-foreground/90',
|
||||
]}
|
||||
>
|
||||
<span class="transition-colors hover:text-primary block">{title}</span>
|
||||
{currentPath === link && (
|
||||
<span class="absolute bottom-0 left-0 w-full h-0.5 bg-primary-500" />
|
||||
)}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div class="md:hidden">
|
||||
<MobileMenu client:idle navigations={navigations} currentPath={currentPath} />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
@@ -0,0 +1,88 @@
|
||||
---
|
||||
import heroContent from '../data/hero-content.json';
|
||||
import heroStats from '../data/hero-stats.json';
|
||||
|
||||
const { communityLabel, headingLine1, headingLine2, description, buttons } = heroContent;
|
||||
---
|
||||
|
||||
<section class="relative w-full py-20 md:py-32 lg:py-40 overflow-hidden">
|
||||
<div class="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div class="absolute top-0 left-0 w-full h-full bg-gradient-to-b from-background to-background/50"></div>
|
||||
<div class="absolute top-1/4 -left-20 w-80 h-80 rounded-full bg-gradient-to-r from-primary/20 to-blue-400/20 blur-3xl opacity-60"></div>
|
||||
<div class="absolute bottom-1/3 -right-20 w-80 h-80 rounded-full bg-gradient-to-r from-blue-400/20 to-primary/20 blur-3xl opacity-60"></div>
|
||||
<div class="absolute inset-0 bg-[linear-gradient(rgba(59,130,246,0.05)_1px,transparent_1px),linear-gradient(to_right,rgba(59,130,246,0.05)_1px,transparent_1px)] bg-[size:40px_40px]"></div>
|
||||
</div>
|
||||
|
||||
<div class="container px-4 md:px-6 relative">
|
||||
<div class="grid gap-6 lg:grid-cols-2 lg:gap-12 items-center">
|
||||
<div class="flex flex-col justify-center space-y-8 animate-[fadeInUp_0.5s_ease-out]">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<span class="inline-flex items-center rounded-full border px-3 py-1 text-sm">
|
||||
{communityLabel}
|
||||
</span>
|
||||
<a
|
||||
href="https://ancikri.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
class="inline-flex items-center rounded-full border px-3 py-1 text-sm hover:bg-muted transition-colors"
|
||||
>
|
||||
Powered By Ancikri
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<h1 class="text-4xl md:text-5xl lg:text-6xl xl:text-7xl font-bold tracking-tighter bg-clip-text text-transparent bg-gradient-to-r from-foreground via-foreground to-foreground/70">
|
||||
{headingLine1}{' '}
|
||||
<span class="bg-clip-text bg-gradient-to-r text-primary-500">
|
||||
{headingLine2}
|
||||
</span>
|
||||
</h1>
|
||||
<p class="max-w-[600px] text-muted-foreground md:text-xl">
|
||||
{description}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row gap-4 w-full sm:w-auto">
|
||||
<a
|
||||
href={buttons.joinUrl}
|
||||
class="inline-flex items-center justify-center rounded-lg bg-primary text-primary-foreground px-6 py-3 text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
{buttons.join}
|
||||
</a>
|
||||
<a
|
||||
href={buttons.exploreUrl}
|
||||
class="inline-flex items-center justify-center rounded-lg border border-input bg-background px-6 py-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors w-full sm:w-auto"
|
||||
>
|
||||
{buttons.explore}
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center md:justify-start gap-6 sm:gap-8">
|
||||
{heroStats.map(({ value, label }: { value: string; label: string }, i: number) => (
|
||||
<>
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="text-2xl font-bold bg-clip-text text-primary-500">
|
||||
{value}
|
||||
</div>
|
||||
<div class="text-xs text-muted-foreground">{label}</div>
|
||||
</div>
|
||||
{i < heroStats.length - 1 && (
|
||||
<div class="hidden sm:block h-10 border-r border-border mx-4"></div>
|
||||
)}
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="relative w-full lg:w-auto mx-auto lg:ml-auto animate-[fadeIn_0.5s_ease-out_0.2s_both]">
|
||||
<img
|
||||
src="/logo.webp"
|
||||
alt="logo"
|
||||
width="600"
|
||||
height="500"
|
||||
class="w-full h-auto object-cover"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,78 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import { LuMenu, LuX } from 'react-icons/lu';
|
||||
|
||||
interface Navigation {
|
||||
title: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
navigations: Navigation[];
|
||||
currentPath: string;
|
||||
}
|
||||
|
||||
export default function MobileMenu({ navigations, currentPath }: Props) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
document.body.style.overflow = 'hidden';
|
||||
} else {
|
||||
document.body.style.overflow = 'auto';
|
||||
}
|
||||
return () => {
|
||||
document.body.style.overflow = 'auto';
|
||||
};
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen((prev) => !prev)}
|
||||
className="relative z-50 p-2 rounded-lg hover:bg-muted transition-colors"
|
||||
aria-label={open ? 'Tutup menu' : 'Buka menu'}
|
||||
>
|
||||
{open ? <LuX className="size-6" /> : <LuMenu className="size-6" />}
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-40 bg-background flex flex-col">
|
||||
<div className="container flex h-20 items-center justify-between">
|
||||
<a
|
||||
href="/"
|
||||
className="relative overflow-hidden rounded"
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
<img src="/logo.webp" alt="IMPHNEN" className="w-24 h-auto" />
|
||||
</a>
|
||||
|
||||
<button
|
||||
onClick={() => setOpen(false)}
|
||||
className="p-2 rounded-lg hover:bg-muted transition-colors"
|
||||
aria-label="Tutup menu"
|
||||
>
|
||||
<LuX className="size-6" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<nav className="flex-1 flex flex-col items-center justify-center gap-6 py-10">
|
||||
{navigations.map(({ link, title }) => (
|
||||
<a
|
||||
key={link}
|
||||
href={link}
|
||||
className={`text-2xl font-medium py-2 px-6 rounded-lg transition-colors ${
|
||||
currentPath === link
|
||||
? 'text-primary bg-primary/10'
|
||||
: 'text-foreground hover:bg-muted'
|
||||
}`}
|
||||
onClick={() => setOpen(false)}
|
||||
>
|
||||
{title}
|
||||
</a>
|
||||
))}
|
||||
</nav>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
export default function RequestFeature() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [description, setDescription] = useState('');
|
||||
const maxDescriptionLength = 10000;
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
onClick={() => setOpen(true)}
|
||||
className="inline-flex items-center justify-center rounded-lg bg-white text-gray-900 px-6 py-3 text-sm font-medium hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Request Fitur
|
||||
</button>
|
||||
|
||||
{open && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div
|
||||
className="absolute inset-0 bg-black/50"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<div className="relative bg-white rounded-xl p-6 w-full max-w-[600px] mx-4 shadow-xl">
|
||||
<h2 className="text-2xl font-bold mb-6">Request Fitur</h2>
|
||||
<form className="grid items-start gap-8">
|
||||
<div className="grid gap-4">
|
||||
<label htmlFor="namaFitur" className="font-medium text-sm">
|
||||
Nama Fitur *
|
||||
</label>
|
||||
<input
|
||||
id="namaFitur"
|
||||
placeholder="Masukkan nama fitur yang diinginkan"
|
||||
className="h-11 rounded-lg border border-input bg-background px-3 text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<label htmlFor="deskripsiFitur" className="font-medium text-sm">
|
||||
Deskripsi Fitur *
|
||||
</label>
|
||||
<span className="text-sm text-gray-500">
|
||||
{description.length}/{maxDescriptionLength}
|
||||
</span>
|
||||
</div>
|
||||
<textarea
|
||||
id="deskripsiFitur"
|
||||
placeholder="Jelaskan fitur yang Anda inginkan secara detail"
|
||||
maxLength={maxDescriptionLength}
|
||||
rows={6}
|
||||
className="min-h-[150px] rounded-lg border border-input bg-background px-3 py-2 text-sm"
|
||||
value={description}
|
||||
onChange={(e) => setDescription(e.target.value)}
|
||||
/>
|
||||
<p className="text-sm text-gray-500 -mt-2">
|
||||
Jelaskan manfaat dan cara kerja fitur yang Anda inginkan
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<button
|
||||
type="submit"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-primary text-primary-foreground px-6 py-3 text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Kirim Permintaan
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen(false)}
|
||||
className="inline-flex items-center justify-center rounded-lg border border-input bg-background px-6 py-3 text-sm font-medium hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { BiUpvote } from 'react-icons/bi';
|
||||
import { FiCheckCircle } from 'react-icons/fi';
|
||||
import { MdOutlineOpenInNew } from 'react-icons/md';
|
||||
|
||||
interface RoadmapItem {
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
status: 'upcoming' | 'in_progress' | 'completed';
|
||||
votes: number;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
export default function RoadmapVote() {
|
||||
const [items, setItems] = useState<RoadmapItem[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [votedIds, setVotedIds] = useState<Set<string>>(new Set());
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://api.imphnen.dev/v1/landing/cms/roadmap')
|
||||
.then((r) => r.json())
|
||||
.then((json) => {
|
||||
setItems(json.data || []);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, []);
|
||||
|
||||
const upcomingItems = items.filter((i) => i.status === 'upcoming');
|
||||
const inProgressItems = items.filter((i) => i.status === 'in_progress');
|
||||
const completedItems = items.filter((i) => i.status === 'completed');
|
||||
|
||||
const handleVote = (id: string) => {
|
||||
const alreadyVoted = votedIds.has(id);
|
||||
|
||||
setItems((prev) =>
|
||||
prev.map((item) =>
|
||||
item.id === id
|
||||
? { ...item, votes: item.votes + (alreadyVoted ? -1 : 1) }
|
||||
: item
|
||||
)
|
||||
);
|
||||
|
||||
if (alreadyVoted) {
|
||||
setVotedIds((prev) => {
|
||||
const next = new Set(prev);
|
||||
next.delete(id);
|
||||
return next;
|
||||
});
|
||||
} else {
|
||||
setVotedIds((prev) => new Set(prev).add(id));
|
||||
fetch(`https://api.imphnen.dev/v1/landing/cms/roadmap/vote/${id}`, {
|
||||
method: 'POST',
|
||||
}).catch(() => {});
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center py-20">
|
||||
<div className="w-8 h-8 border-3 border-gray-200 border-t-primary-500 rounded-full animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 max-w-7xl mx-auto">
|
||||
{/* Vote Now column */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-amber-100 text-amber-600 rounded-lg">
|
||||
🔥
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Vote Now</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{upcomingItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all rounded-xl overflow-hidden"
|
||||
>
|
||||
<div className="p-4 pb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-gray-600 text-sm mb-4">{item.description}</p>
|
||||
<div className="flex items-center justify-between border-t border-gray-100 pt-3">
|
||||
<button
|
||||
onClick={() => handleVote(item.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${
|
||||
votedIds.has(item.id)
|
||||
? 'bg-primary-500 text-white hover:bg-primary-600'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
}`}
|
||||
>
|
||||
<BiUpvote
|
||||
className={`w-4 h-4 ${
|
||||
votedIds.has(item.id) ? 'text-white' : 'text-gray-600'
|
||||
}`}
|
||||
/>
|
||||
<span>{votedIds.has(item.id) ? 'Voted' : 'Vote'}</span>
|
||||
</button>
|
||||
<div className="flex items-center gap-2 bg-gray-50 px-3 py-1.5 rounded-lg">
|
||||
<BiUpvote className="w-4 h-4 text-gray-500" />
|
||||
<span className="text-sm font-medium text-gray-700">
|
||||
{item.votes}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{upcomingItems.length === 0 && (
|
||||
<p className="text-gray-500 text-sm px-2">No upcoming items yet.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* In Progress column */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-primary-100 text-primary-600 rounded-lg">
|
||||
🧑🔧
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">In Progress</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{inProgressItems.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 hover:border-primary-200 shadow-sm hover:shadow-md transition-all rounded-xl overflow-hidden"
|
||||
>
|
||||
<div className="p-4 pb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-gray-600 text-sm">{item.description}</p>
|
||||
<div className="mt-4 pt-3 border-t border-gray-100">
|
||||
<div className="w-full bg-gray-200 rounded-full h-1.5">
|
||||
<div
|
||||
className="bg-primary-500 h-1.5 rounded-full"
|
||||
style={{ width: `${(index + 1) * 33}%` }}
|
||||
></div>
|
||||
</div>
|
||||
<p className="text-xs text-gray-500 mt-2">In development</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{inProgressItems.length === 0 && (
|
||||
<p className="text-gray-500 text-sm px-2">Nothing in progress.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Completed column */}
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3 px-2">
|
||||
<div className="flex items-center justify-center w-10 h-10 bg-green-100 text-green-600 rounded-lg">
|
||||
🤓
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold text-gray-900">Completed</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
{completedItems.map((item) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-white border border-gray-200 hover:border-green-200 shadow-sm hover:shadow-md transition-all rounded-xl overflow-hidden"
|
||||
>
|
||||
<div className="p-4 pb-3">
|
||||
<h3 className="text-lg font-semibold text-gray-900">
|
||||
{item.title}
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-4 pb-4">
|
||||
<p className="text-gray-600 text-sm mb-3">{item.description}</p>
|
||||
<div className="flex justify-between">
|
||||
<div className="flex items-center gap-2 text-green-600">
|
||||
<FiCheckCircle className="w-4 h-4" />
|
||||
<span className="text-sm font-medium">Implemented</span>
|
||||
</div>
|
||||
<button className="inline-flex items-center gap-2 rounded-lg bg-primary text-primary-foreground px-3 py-1.5 text-sm font-medium hover:bg-primary/90 transition-colors">
|
||||
Coba sekarang
|
||||
<MdOutlineOpenInNew className="size-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
{completedItems.length === 0 && (
|
||||
<p className="text-gray-500 text-sm px-2">No completed items yet.</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { FaQuoteLeft } from 'react-icons/fa';
|
||||
|
||||
interface ApiTestimonial {
|
||||
id: number;
|
||||
user_id: number;
|
||||
user_fullname: string;
|
||||
role: string;
|
||||
content: string;
|
||||
created_at: string;
|
||||
is_deleted: boolean;
|
||||
}
|
||||
|
||||
interface Testimonial {
|
||||
id: number;
|
||||
name: string;
|
||||
role: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
const AVATAR_COLORS = [
|
||||
'bg-primary-500 text-white',
|
||||
'bg-blue-500 text-white',
|
||||
'bg-green-500 text-white',
|
||||
'bg-purple-500 text-white',
|
||||
'bg-orange-500 text-white',
|
||||
'bg-pink-500 text-white',
|
||||
];
|
||||
|
||||
function getAvatarColor(index: number) {
|
||||
return AVATAR_COLORS[index % AVATAR_COLORS.length];
|
||||
}
|
||||
|
||||
function getInitial(name: string) {
|
||||
return name.charAt(0).toUpperCase();
|
||||
}
|
||||
|
||||
export default function TestimonialSection() {
|
||||
const [testimonials, setTestimonials] = useState<Testimonial[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
fetch('https://api.imphnen.dev/v1/landing/cms/testimonials')
|
||||
.then((res) => (res.ok ? res.json() : Promise.reject()))
|
||||
.then((json) => {
|
||||
setTestimonials(
|
||||
(json.data as ApiTestimonial[])
|
||||
.filter((t) => !t.is_deleted)
|
||||
.slice(0, 6)
|
||||
.map((t) => ({
|
||||
id: t.id,
|
||||
name: t.user_fullname,
|
||||
role: t.role,
|
||||
text: t.content,
|
||||
}))
|
||||
);
|
||||
})
|
||||
.catch(() => setTestimonials([]));
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<section className="w-full py-20 md:py-28 bg-gray-50">
|
||||
<div className="container">
|
||||
<div className="flex flex-col items-center justify-center space-y-4 text-center mb-16">
|
||||
<h2 className="text-3xl font-semibold tracking-tight md:text-4xl animate-[fadeInUp_0.4s_ease-out]">
|
||||
Apa Kata Mereka Tentang
|
||||
<span className="block mt-2 text-primary-500">Komunitas Kami?</span>
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-8 md:gap-6 grid-cols-1 md:grid-cols-2 lg:grid-cols-3 mb-16">
|
||||
{testimonials.map((testimonial, index) => (
|
||||
<div
|
||||
key={testimonial.id}
|
||||
className="p-6 bg-white rounded-xl shadow-sm hover:shadow-md transition-shadow duration-300 animate-[fadeInUp_0.4s_ease-out]"
|
||||
style={{
|
||||
animationDelay: `${index * 100}ms`,
|
||||
animationFillMode: 'both',
|
||||
}}
|
||||
>
|
||||
<div className="flex flex-col space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className={`w-12 h-12 rounded-full flex items-center justify-center text-lg font-semibold ${getAvatarColor(index)}`}
|
||||
>
|
||||
{getInitial(testimonial.name)}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-gray-900">
|
||||
{testimonial.name}
|
||||
</h4>
|
||||
<p className="text-sm text-gray-600">{testimonial.role}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-gray-600 relative">
|
||||
<FaQuoteLeft className="text-primary-500/30 w-6 h-6 mb-2" />
|
||||
<p className="text-sm/relaxed">{testimonial.text}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-center">
|
||||
<a
|
||||
href="/testimonials"
|
||||
className="inline-flex items-center justify-center rounded-lg bg-primary text-primary-foreground px-6 py-3 text-sm font-medium hover:bg-primary/90 transition-colors"
|
||||
>
|
||||
Tulis Testimonimu
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user