From 95bf09c2fa57c5f354abf3b1fe91f6853ccbfb19 Mon Sep 17 00:00:00 2001 From: maulanasdqn Date: Fri, 10 Apr 2026 22:10:52 +0700 Subject: [PATCH] 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 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) --- apps/landing/.swcrc | 30 - apps/landing/astro.config.mjs | 11 + apps/landing/eslint.config.mjs | 21 - apps/landing/index.d.ts | 6 - apps/landing/next-env.d.ts | 6 - apps/landing/next.config.js | 10 - apps/landing/package.json | 19 + apps/landing/postcss.config.mjs | 7 - apps/landing/project.json | 20 +- .../(home)/_components/community-section.tsx | 73 - .../(home)/_components/cta-section.tsx | 91 - .../(home)/_components/hero-section.tsx | 109 - apps/landing/src/app/(public)/(home)/page.tsx | 14 - .../src/app/(public)/_components/footer.tsx | 126 - .../src/app/(public)/_components/header.tsx | 128 - .../app/(public)/_components/theme-toggle.tsx | 39 - apps/landing/src/app/(public)/events/page.tsx | 241 -- .../src/app/(public)/hackathon/page.tsx | 159 - apps/landing/src/app/(public)/layout.tsx | 12 - .../_components/feature-request-cta.tsx | 35 - .../roadmap/_components/projects-vote.tsx | 251 -- .../_components/request-feature-popup.tsx | 120 - .../landing/src/app/(public)/roadmap/page.tsx | 11 - apps/landing/src/app/(public)/teams/page.tsx | 3 - .../src/app/(public)/testimonials/page.tsx | 98 - .../app/(public)/testimonials/submit/page.tsx | 3 - apps/landing/src/app/_components/logo.tsx | 71 - .../landing/src/app/_components/providers.tsx | 17 - apps/landing/src/app/_components/toaster.tsx | 28 - .../landing/src/app/daftar-hackathon/page.tsx | 6 - apps/landing/src/app/layout.tsx | 52 - apps/landing/src/components/CTASection.astro | 56 + .../src/components/CommunitySection.astro | 66 + apps/landing/src/components/Footer.astro | 80 + apps/landing/src/components/Header.astro | 35 + apps/landing/src/components/HeroSection.astro | 88 + apps/landing/src/components/MobileMenu.tsx | 78 + .../landing/src/components/RequestFeature.tsx | 81 + apps/landing/src/components/RoadmapVote.tsx | 205 ++ .../TestimonialSection.tsx} | 35 +- apps/landing/src/hooks/use-media-query.ts | 30 - apps/landing/src/layouts/Base.astro | 51 + apps/landing/src/layouts/Public.astro | 22 + apps/landing/src/lib/cookies.ts | 24 - apps/landing/src/lib/fetcher.ts | 6 - apps/landing/src/lib/fonts.ts | 13 - apps/landing/src/lib/headers.ts | 10 - apps/landing/src/lib/rpc.ts | 8 - apps/landing/src/pages/daftar-hackathon.astro | 13 + apps/landing/src/pages/events.astro | 196 ++ apps/landing/src/pages/hackathon.astro | 145 + apps/landing/src/pages/index.astro | 14 + apps/landing/src/pages/roadmap.astro | 24 + apps/landing/src/pages/teams.astro | 9 + .../src/pages/testimonials/index.astro | 84 + .../src/pages/testimonials/submit.astro | 9 + apps/landing/src/styles/globals.css | 11 +- apps/landing/tsconfig.json | 46 +- flake.nix | 70 +- package-lock.json | 3071 +++++++++++++++-- package.json | 3 + 61 files changed, 4141 insertions(+), 2259 deletions(-) delete mode 100644 apps/landing/.swcrc create mode 100644 apps/landing/astro.config.mjs delete mode 100644 apps/landing/eslint.config.mjs delete mode 100644 apps/landing/index.d.ts delete mode 100644 apps/landing/next-env.d.ts delete mode 100644 apps/landing/next.config.js create mode 100644 apps/landing/package.json delete mode 100644 apps/landing/postcss.config.mjs delete mode 100644 apps/landing/src/app/(public)/(home)/_components/community-section.tsx delete mode 100644 apps/landing/src/app/(public)/(home)/_components/cta-section.tsx delete mode 100644 apps/landing/src/app/(public)/(home)/_components/hero-section.tsx delete mode 100644 apps/landing/src/app/(public)/(home)/page.tsx delete mode 100644 apps/landing/src/app/(public)/_components/footer.tsx delete mode 100644 apps/landing/src/app/(public)/_components/header.tsx delete mode 100644 apps/landing/src/app/(public)/_components/theme-toggle.tsx delete mode 100644 apps/landing/src/app/(public)/events/page.tsx delete mode 100644 apps/landing/src/app/(public)/hackathon/page.tsx delete mode 100644 apps/landing/src/app/(public)/layout.tsx delete mode 100644 apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx delete mode 100644 apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx delete mode 100644 apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx delete mode 100644 apps/landing/src/app/(public)/roadmap/page.tsx delete mode 100644 apps/landing/src/app/(public)/teams/page.tsx delete mode 100644 apps/landing/src/app/(public)/testimonials/page.tsx delete mode 100644 apps/landing/src/app/(public)/testimonials/submit/page.tsx delete mode 100644 apps/landing/src/app/_components/logo.tsx delete mode 100644 apps/landing/src/app/_components/providers.tsx delete mode 100644 apps/landing/src/app/_components/toaster.tsx delete mode 100644 apps/landing/src/app/daftar-hackathon/page.tsx delete mode 100644 apps/landing/src/app/layout.tsx create mode 100644 apps/landing/src/components/CTASection.astro create mode 100644 apps/landing/src/components/CommunitySection.astro create mode 100644 apps/landing/src/components/Footer.astro create mode 100644 apps/landing/src/components/Header.astro create mode 100644 apps/landing/src/components/HeroSection.astro create mode 100644 apps/landing/src/components/MobileMenu.tsx create mode 100644 apps/landing/src/components/RequestFeature.tsx create mode 100644 apps/landing/src/components/RoadmapVote.tsx rename apps/landing/src/{app/(public)/(home)/_components/testimonial-section.tsx => components/TestimonialSection.tsx} (75%) delete mode 100644 apps/landing/src/hooks/use-media-query.ts create mode 100644 apps/landing/src/layouts/Base.astro create mode 100644 apps/landing/src/layouts/Public.astro delete mode 100644 apps/landing/src/lib/cookies.ts delete mode 100644 apps/landing/src/lib/fetcher.ts delete mode 100644 apps/landing/src/lib/fonts.ts delete mode 100644 apps/landing/src/lib/headers.ts delete mode 100644 apps/landing/src/lib/rpc.ts create mode 100644 apps/landing/src/pages/daftar-hackathon.astro create mode 100644 apps/landing/src/pages/events.astro create mode 100644 apps/landing/src/pages/hackathon.astro create mode 100644 apps/landing/src/pages/index.astro create mode 100644 apps/landing/src/pages/roadmap.astro create mode 100644 apps/landing/src/pages/teams.astro create mode 100644 apps/landing/src/pages/testimonials/index.astro create mode 100644 apps/landing/src/pages/testimonials/submit.astro diff --git a/apps/landing/.swcrc b/apps/landing/.swcrc deleted file mode 100644 index ac23c93..0000000 --- a/apps/landing/.swcrc +++ /dev/null @@ -1,30 +0,0 @@ -{ - "jsc": { - "target": "es2017", - "parser": { - "syntax": "typescript", - "decorators": true, - "dynamicImport": true - }, - "transform": { - "decoratorMetadata": true, - "legacyDecorator": true - }, - "keepClassNames": true, - "externalHelpers": true, - "loose": true - }, - "module": { - "type": "commonjs" - }, - "sourceMaps": true, - "exclude": [ - "jest.config.ts", - ".*\\.spec.tsx?$", - ".*\\.test.tsx?$", - "./src/jest-setup.ts$", - "./**/jest-setup.ts$", - ".*.js$", - ".*.d.ts$" - ] -} diff --git a/apps/landing/astro.config.mjs b/apps/landing/astro.config.mjs new file mode 100644 index 0000000..f2ada92 --- /dev/null +++ b/apps/landing/astro.config.mjs @@ -0,0 +1,11 @@ +import { defineConfig } from 'astro/config'; +import tailwindcss from '@tailwindcss/vite'; +import react from '@astrojs/react'; + +export default defineConfig({ + integrations: [react()], + vite: { + plugins: [tailwindcss()], + }, + output: 'static', +}); diff --git a/apps/landing/eslint.config.mjs b/apps/landing/eslint.config.mjs deleted file mode 100644 index 0551fe2..0000000 --- a/apps/landing/eslint.config.mjs +++ /dev/null @@ -1,21 +0,0 @@ -import { FlatCompat } from '@eslint/eslintrc'; -import { dirname } from 'path'; -import { fileURLToPath } from 'url'; -import js from '@eslint/js'; -import { fixupConfigRules } from '@eslint/compat'; -import nx from '@nx/eslint-plugin'; -import baseConfig from '../../eslint.config.mjs'; -const compat = new FlatCompat({ - baseDirectory: dirname(fileURLToPath(import.meta.url)), - recommendedConfig: js.configs.recommended, -}); - -export default [ - ...fixupConfigRules(compat.extends('next')), - ...fixupConfigRules(compat.extends('next/core-web-vitals')), - ...baseConfig, - ...nx.configs['flat/react-typescript'], - { - ignores: ['.next/**/*'], - }, -]; diff --git a/apps/landing/index.d.ts b/apps/landing/index.d.ts deleted file mode 100644 index 7ba08fa..0000000 --- a/apps/landing/index.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -declare module '*.svg' { - const content: any; - export const ReactComponent: any; - export default content; -} diff --git a/apps/landing/next-env.d.ts b/apps/landing/next-env.d.ts deleted file mode 100644 index 375b99d..0000000 --- a/apps/landing/next-env.d.ts +++ /dev/null @@ -1,6 +0,0 @@ -/// -/// -import "./../../dist/apps/landing/.next/types/routes.d.ts"; - -// NOTE: This file should not be edited -// see https://nextjs.org/docs/app/api-reference/config/typescript for more information. diff --git a/apps/landing/next.config.js b/apps/landing/next.config.js deleted file mode 100644 index 38d91b7..0000000 --- a/apps/landing/next.config.js +++ /dev/null @@ -1,10 +0,0 @@ -const { composePlugins, withNx } = require('@nx/next'); -const path = require('path'); - -/** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */ -const nextConfig = { - nx: {}, - output: 'standalone', -}; - -module.exports = composePlugins(withNx)(nextConfig); diff --git a/apps/landing/package.json b/apps/landing/package.json new file mode 100644 index 0000000..99530b8 --- /dev/null +++ b/apps/landing/package.json @@ -0,0 +1,19 @@ +{ + "name": "landing", + "version": "0.0.0", + "private": true, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "astro": "^5.7.10", + "@astrojs/react": "^4.2.1", + "@tailwindcss/vite": "^4.1.4", + "tailwindcss": "^4.1.4", + "react": "^19.1.0", + "react-dom": "^19.1.0", + "react-icons": "^5.5.0" + } +} diff --git a/apps/landing/postcss.config.mjs b/apps/landing/postcss.config.mjs deleted file mode 100644 index 297374d..0000000 --- a/apps/landing/postcss.config.mjs +++ /dev/null @@ -1,7 +0,0 @@ -const config = { - plugins: { - '@tailwindcss/postcss': {}, - }, -}; - -export default config; diff --git a/apps/landing/project.json b/apps/landing/project.json index 88a66fe..e5d22cd 100644 --- a/apps/landing/project.json +++ b/apps/landing/project.json @@ -1,24 +1,12 @@ { "name": "landing", "$schema": "../../node_modules/nx/schemas/project-schema.json", - "sourceRoot": "apps/landing", + "sourceRoot": "apps/landing/src", "projectType": "application", "tags": [], - "// targets": "to see all targets run: nx show project landing --web", "targets": { - "serve": { - "executor": "@nx/next:server", - "options": { - "buildTarget": "landing:build", - "dev": true - } - }, - "build": { - "executor": "@nx/next:build", - "outputs": ["{options.outputPath}"], - "options": { - "outputPath": "dist/apps/landing" - } - } + "dev": { "command": "cd apps/landing && npx astro dev" }, + "build": { "command": "cd apps/landing && npx astro build" }, + "preview": { "command": "cd apps/landing && npx astro preview" } } } diff --git a/apps/landing/src/app/(public)/(home)/_components/community-section.tsx b/apps/landing/src/app/(public)/(home)/_components/community-section.tsx deleted file mode 100644 index bf8b808..0000000 --- a/apps/landing/src/app/(public)/(home)/_components/community-section.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import SOCIALS from '@/data/socials.json'; -import { - FaArrowRight, - FaDiscord, - FaGithub, - FaFacebook, - FaInstagram, - FaLinkedin, - FaTiktok, -} from 'react-icons/fa'; - -const ICON_MAP: Record = { - FaFacebook, FaDiscord, FaGithub, FaInstagram, FaTiktok, FaLinkedin, -}; - -const COLOR_MAP: Record = { - FaFacebook: 'text-[#1877F2]', - FaDiscord: 'text-[#5865F2]', - FaGithub: 'text-[#000000]', - FaInstagram: 'text-[#E4405F]', - FaTiktok: 'text-[#000000]', - FaLinkedin: 'text-[#0A66C2]', -}; - -export function CommunitySection() { - return ( -
-
-
-

- Bergabung dengan Komunitas Kami di - Berbagai Platform -

-

- Terhubung dengan sesama developer di komunitas kami -

-
- -
- {SOCIALS.map((community, index) => { - const IconComponent = ICON_MAP[community.icon] || FaArrowRight; - const iconColor = COLOR_MAP[community.icon] || 'text-primary-500'; - - return ( -
-
-
- -

{community.name}

-
-

{community.description}

- - Jelajahi Komunitas - - -
-
- ); - })} -
-
-
- ); -} diff --git a/apps/landing/src/app/(public)/(home)/_components/cta-section.tsx b/apps/landing/src/app/(public)/(home)/_components/cta-section.tsx deleted file mode 100644 index 16f604c..0000000 --- a/apps/landing/src/app/(public)/(home)/_components/cta-section.tsx +++ /dev/null @@ -1,91 +0,0 @@ -'use client'; - -import { LogoSimple } from '@/app/_components/logo'; -import { motion, useInView } from 'framer-motion'; -import { useRef } from 'react'; -import { FaArrowRight } from 'react-icons/fa'; - -export function CTASection() { - const ref = useRef(null); - const isInView = useInView(ref, { once: true, amount: 0.2 }); - - return ( -
- -
- -
-
- -
- -

- LET'S GO - - SAAT - NYA - - KAMU JOIN! -

-
- - - Jadilah bagian dari komunitas developer terbesar di Indonesia. - Tingkatkan skill, perluas jaringan, dan raih kesempatan karir - bersama kami! - - - - - Join Sekarang - - - -
- - -
-
-
-
- -
-

250.000+

-

Programmer Sudah Bergabung

-
-
-
-
-
-
-
-
- ); -} diff --git a/apps/landing/src/app/(public)/(home)/_components/hero-section.tsx b/apps/landing/src/app/(public)/(home)/_components/hero-section.tsx deleted file mode 100644 index 0c4f8f7..0000000 --- a/apps/landing/src/app/(public)/(home)/_components/hero-section.tsx +++ /dev/null @@ -1,109 +0,0 @@ -'use client'; - -import HERO_CONTENT from '@/data/hero-content.json'; -import HERO_STATS from '@/data/hero-stats.json'; -import { Button } from '@components'; -import Image from 'next/image'; -import { useRouter } from 'next/navigation'; -import { Fragment } from 'react'; - -export function HeroSection() { - const router = useRouter(); - const { communityLabel, headingLine1, headingLine2, description, buttons } = - HERO_CONTENT; - - return ( -
-
-
-
-
-
-
- -
-
-
-
- - {communityLabel} - - - Powered By Ancikri - -
- -
-

- {headingLine1}{' '} - - {headingLine2} - -

-

- {description} -

-
- -
- - -
- -
- {HERO_STATS.map(({ value, label }, i) => ( - -
-
- {value} -
-
{label}
-
- {i < HERO_STATS.length - 1 && ( -
- )} - - ))} -
-
- -
- logo -
-
-
- - -
- ); -} diff --git a/apps/landing/src/app/(public)/(home)/page.tsx b/apps/landing/src/app/(public)/(home)/page.tsx deleted file mode 100644 index 535f374..0000000 --- a/apps/landing/src/app/(public)/(home)/page.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { CommunitySection } from './_components/community-section'; -import { CTASection } from './_components/cta-section'; -import { HeroSection } from './_components/hero-section'; -import { TestimonialSection } from './_components/testimonial-section'; -export default function Page() { - return ( - <> - - - - - - ); -} diff --git a/apps/landing/src/app/(public)/_components/footer.tsx b/apps/landing/src/app/(public)/_components/footer.tsx deleted file mode 100644 index 395c71b..0000000 --- a/apps/landing/src/app/(public)/_components/footer.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import { LogoSimple } from '@/app/_components/logo'; -import NAVIGATIONS from '@/data/navigations.json'; -import SOCIALS from '@/data/socials.json'; -import Link from 'next/link'; -import { - FaDiscord, - FaFacebook, - FaGithub, - FaInstagram, - FaLinkedinIn, - FaTiktok, -} from 'react-icons/fa'; - -export default function Footer() { - return ( -
-
-
-
-
- -
-

- Ingin Menjadi Programmer Handal Namun Enggan Ngoding -

-
- - - - - - - - - - - - - - - - - - -
-
-
-

Halaman

-
    - {NAVIGATIONS.map(({ link, title }) => ( -
  • - - {title} - -
  • - ))} -
-
-
-

Link

-
    - {SOCIALS.map(({ link, name }) => ( -
  • - - {name} - -
  • - ))} -
-
-
-

Partners

-
    -
    -
    -
    -

    - © {new Date().getFullYear()} IMPHNEN - Ingin Menjadi Programmer - Handal, Namun Enggan Ngoding. All rights reserved. -

    -

    - Powered by{' '} - - Ancikri - -

    -
    -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/_components/header.tsx b/apps/landing/src/app/(public)/_components/header.tsx deleted file mode 100644 index e73ecc5..0000000 --- a/apps/landing/src/app/(public)/_components/header.tsx +++ /dev/null @@ -1,128 +0,0 @@ -'use client'; - -import { LogoSimple } from '@/app/_components/logo'; -import NAVIGATIONS from '@/data/navigations.json'; -import { cn } from '@utils'; -import { AnimatePresence, motion } from 'framer-motion'; -import Link from 'next/link'; -import { usePathname } from 'next/navigation'; -import { useEffect, useState } from 'react'; -import { LuMenu, LuX } from 'react-icons/lu'; - -export function Header() { - const pathname = usePathname(); - const [mobileMenuOpen, setMobileMenuOpen] = useState(false); - - useEffect(() => { - if (mobileMenuOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'auto'; - } - return () => { - document.body.style.overflow = 'auto'; - }; - }, [mobileMenuOpen]); - - useEffect(() => { - setMobileMenuOpen(false); - }, [pathname]); - - return ( -
    -
    - - - - - - - - - - {mobileMenuOpen && ( - -
    - setMobileMenuOpen(false)} - > - - - - -
    - - - {NAVIGATIONS.map(({ link, title }) => ( - setMobileMenuOpen(false)} - > - {title} - - ))} - -
    - )} -
    -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/_components/theme-toggle.tsx b/apps/landing/src/app/(public)/_components/theme-toggle.tsx deleted file mode 100644 index f6f0feb..0000000 --- a/apps/landing/src/app/(public)/_components/theme-toggle.tsx +++ /dev/null @@ -1,39 +0,0 @@ -'use client'; - -import { Button } from '@components'; -import { useTheme } from 'next-themes'; -import { useEffect, useState } from 'react'; -import { LuMoon, LuSun } from 'react-icons/lu'; - -export function ThemeToggle() { - const { theme, setTheme } = useTheme(); - const [mounted, setMounted] = useState(false); - - useEffect(() => { - setMounted(true); - }, []); - - if (!mounted) { - return null; - } - - const toggleTheme = () => { - setTheme(theme === 'dark' ? 'light' : 'dark'); - }; - - return ( - - ); -} diff --git a/apps/landing/src/app/(public)/events/page.tsx b/apps/landing/src/app/(public)/events/page.tsx deleted file mode 100644 index 156f66a..0000000 --- a/apps/landing/src/app/(public)/events/page.tsx +++ /dev/null @@ -1,241 +0,0 @@ -'use client'; - -import { buttonVariants } from '@components'; -import { cn } from '@utils'; -import { useEffect, useState } from 'react'; -import { HiCalendar, HiClock, HiLocationMarker } from 'react-icons/hi'; - -interface ApiEvent { - id: number; - name: string; - description: string; - detail_link: string; - price: number; - is_online: boolean; - is_deleted: boolean; - location: string; - start_date: string; - end_date: string; - created_at: string; - updated_at: string; -} - -interface ApiResponse { - data: ApiEvent[]; - meta: Record; - version: string; -} - -const formatDate = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleDateString('id-ID', { - day: 'numeric', - month: 'long', - year: 'numeric', - }); -}; - -const formatTime = (dateString: string) => { - const date = new Date(dateString); - return date.toLocaleTimeString('id-ID', { - hour: '2-digit', - minute: '2-digit', - timeZone: 'Asia/Jakarta', - }); -}; - -const getEventStatus = (endDate: string) => { - const now = new Date(); - const end = new Date(endDate); - return end > now ? 'upcoming' : 'past'; -}; - -export default function EventsPage() { - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - fetch('https://api.imphnen.dev/v1/landing/cms/events') - .then((res) => res.json()) - .then((json: ApiResponse) => { - setEvents(json.data?.filter((e) => !e.is_deleted) || []); - }) - .catch(() => setEvents([])) - .finally(() => setLoading(false)); - }, []); - - const sortedEvents = [...events].sort( - (a, b) => - new Date(b.start_date).getTime() - new Date(a.start_date).getTime() - ); - - if (loading) { - return ( -
    -
    -
    -
    -
    - ); - } - - if (sortedEvents.length === 0) { - return ( -
    -

    - Belum ada event tersedia. -

    -
    - ); - } - - return ( -
    -
    - -
    -
    -
    -
    - - Event Terbaru - - - {getEventStatus(sortedEvents[0].end_date) === 'upcoming' - ? 'Upcoming' - : 'Selesai'} - - - {sortedEvents[0].is_online ? 'Online' : 'Onsite'} - -
    -

    - {sortedEvents[0].name} -

    -
    -
    - - {formatDate(sortedEvents[0].start_date)} -
    -
    - - - {formatTime(sortedEvents[0].start_date)} -{' '} - {formatTime(sortedEvents[0].end_date)} WIB - -
    -
    - - - {sortedEvents[0].is_online - ? 'Online' - : sortedEvents[0].location} - -
    - {sortedEvents[0].price > 0 && ( -
    - Rp {sortedEvents[0].price.toLocaleString('id-ID')} -
    - )} -
    -

    - {sortedEvents[0].description} -

    - - Lihat Detail - -
    -
    -
    - - {sortedEvents.slice(1).map((event) => ( -
    -
    -
    - - {getEventStatus(event.end_date) === 'upcoming' - ? 'Upcoming' - : 'Selesai'} - - - {event.is_online ? 'Online' : 'Onsite'} - -
    -

    - {event.name} -

    -
    -
    - - {formatDate(event.start_date)} -
    -
    - - - {event.is_online ? 'Online' : event.location} - -
    - {event.price > 0 && ( -
    - Rp {event.price.toLocaleString('id-ID')} -
    - )} -
    -

    - {event.description} -

    - - Lihat Detail - -
    -
    - ))} -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/hackathon/page.tsx b/apps/landing/src/app/(public)/hackathon/page.tsx deleted file mode 100644 index f6c9ac0..0000000 --- a/apps/landing/src/app/(public)/hackathon/page.tsx +++ /dev/null @@ -1,159 +0,0 @@ -'use client'; - -import { buttonVariants } from '@components'; -import { cn } from '@utils'; -import { useEffect, useState } from 'react'; -import { HiOutlineCode, HiOutlineTrophy } from 'react-icons/hi'; -import { motion } from 'framer-motion'; - -interface TeamItem { - id: string; - name: string; - description: string; - city: string; - logo: string | null; - banner: string | null; -} - -interface WinnerItem { - id: string; - team_id: string; - team_name: string; - rank: number; - prize: string | null; -} - -export default function HackathonsPage() { - const [teams, setTeams] = useState([]); - const [winners, setWinners] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - Promise.all([ - fetch('https://api.imphnen.dev/v1/hackathon/teams/browse?per_page=20') - .then((r) => r.json()) - .then((j) => j.data?.data || []) - .catch(() => []), - fetch('https://api.imphnen.dev/v1/hackathon/winners') - .then((r) => r.json()) - .then((j) => j.data || []) - .catch(() => []), - ]).then(([t, w]) => { - setTeams(t); - setWinners(w); - setLoading(false); - }); - }, []); - - const getWinnerRank = (teamId: string) => { - const w = winners.find((x) => x.team_id === teamId); - return w ? w.rank : null; - }; - - if (loading) { - return ( -
    -
    -
    - ); - } - - if (teams.length === 0) { - return ( -
    -
    -

    No hackathon teams yet.

    - - Join Hackathon - -
    -
    - ); - } - - return ( -
    -
    -

    IMPHNEN Hackathon

    -

    - Tim-tim yang berpartisipasi dalam hackathon IMPHNEN -

    -
    - - {winners.length > 0 && ( -
    -

    - Pemenang -

    -
    - {winners.sort((a, b) => a.rank - b.rank).map((w) => ( -
    -
    {w.rank === 1 ? '🥇' : w.rank === 2 ? '🥈' : '🥉'}
    -

    {w.team_name}

    -

    Juara {w.rank}

    - {w.prize &&

    {w.prize}

    } -
    - ))} -
    -
    - )} - -

    Semua Tim

    - - {teams.map((team, idx) => { - const rank = getWinnerRank(team.id); - return ( - - {team.banner ? ( -
    - {team.name} -
    - ) : ( -
    - -
    - )} -
    -
    -

    {team.name}

    - {rank && ( - - Juara {rank} - - )} -
    - {team.city && ( -

    {team.city}

    - )} - {team.description && ( -

    {team.description}

    - )} - - Lihat Tim - -
    -
    - ); - })} -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/layout.tsx b/apps/landing/src/app/(public)/layout.tsx deleted file mode 100644 index b09f5e5..0000000 --- a/apps/landing/src/app/(public)/layout.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import Footer from './_components/footer'; -import { Header } from './_components/header'; - -export default function Layout({ children }: { children: React.ReactNode }) { - return ( -
    -
    -
    {children}
    -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx deleted file mode 100644 index e39d837..0000000 --- a/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx +++ /dev/null @@ -1,35 +0,0 @@ -'use client'; - -import { motion } from 'framer-motion'; -import { RequestFeaturePopup } from './request-feature-popup'; - -export function FeatureRequestCTA() { - return ( - -
    - - Punya Ide Bagus untuk IMPHNEN? - - - Bagikan ide fitur yang kamu inginkan dan vote untuk membuatnya nyata! - - -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx deleted file mode 100644 index 35cf010..0000000 --- a/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx +++ /dev/null @@ -1,251 +0,0 @@ -'use client'; - -import { Button, Card, CardContent, CardHeader, CardTitle } from '@components'; -import { AnimatePresence, motion } from 'framer-motion'; -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 ProjectsVote() { - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(true); - const [votedIds, setVotedIds] = useState>(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); - - // Optimistic update - 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(() => {}); - } - }; - - const containerVariants = { - hidden: { opacity: 0 }, - visible: { - opacity: 1, - transition: { - staggerChildren: 0.05, - }, - }, - }; - - const itemVariants = { - hidden: { opacity: 0, y: 10 }, - visible: { opacity: 1, y: 0, transition: { duration: 0.2 } }, - }; - - if (loading) { - return ( -
    -
    -
    - ); - } - - return ( -
    - -
    -
    -
    - 🔥 -
    -

    Vote Now

    -
    - - - - {upcomingItems.map((item) => ( - - - - - {item.title} - - - -

    - {item.description} -

    -
    - 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' - }`} - > - - - - {votedIds.has(item.id) ? 'Voted' : 'Vote'} - -
    - - - {item.votes} - -
    -
    -
    -
    -
    - ))} - {upcomingItems.length === 0 && ( -

    No upcoming items yet.

    - )} -
    -
    -
    - -
    -
    -
    - 🧑‍🔧 -
    -

    In Progress

    -
    - - - - {inProgressItems.map((item, index) => ( - - - - - {item.title} - - - -

    {item.description}

    -
    -
    -
    -
    -

    In development

    -
    -
    -
    -
    - ))} - {inProgressItems.length === 0 && ( -

    Nothing in progress.

    - )} -
    -
    -
    - -
    -
    -
    - 🤓 -
    -

    Completed

    -
    - - - - {completedItems.map((item) => ( - - - - - {item.title} - - - -

    - {item.description} -

    -
    -
    - - Implemented -
    - -
    -
    -
    -
    - ))} - {completedItems.length === 0 && ( -

    No completed items yet.

    - )} -
    -
    -
    -
    - ); -} diff --git a/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx deleted file mode 100644 index 13dbfdf..0000000 --- a/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx +++ /dev/null @@ -1,120 +0,0 @@ -'use client'; - -import * as React from 'react'; - -import { useMediaQuery } from '@/hooks/use-media-query'; -import { - Button, - Dialog, - DialogContent, - DialogHeader, - DialogTitle, - DialogTrigger, - Drawer, - DrawerClose, - DrawerContent, - DrawerFooter, - DrawerHeader, - DrawerTitle, - DrawerTrigger, - Input, - Label, - Textarea, -} from '@components'; -import { cn } from '@utils'; - -export function RequestFeaturePopup() { - const [open, setOpen] = React.useState(false); - const isDesktop = useMediaQuery('(min-width: 768px)'); - - if (isDesktop) { - return ( - - - - - - - Request Fitur - - - - - ); - } - - return ( - - - - - - - Request Fitur - - - - - - - - - - ); -} - -interface ProfileFormProps extends React.ComponentProps<'form'> { - showFooterButton?: boolean; -} - -function ProfileForm({ - className, - showFooterButton = false, -}: ProfileFormProps) { - const [description, setDescription] = React.useState(''); - const maxDescriptionLength = 10000; - - return ( -
    -
    - - -
    - -
    -
    - - - {description.length}/{maxDescriptionLength} - -
    -