From 2c347d1cc5292541daf991c06e88de0c3c63d9ca Mon Sep 17 00:00:00 2001 From: Rasyid <49753444+arraysid@users.noreply.github.com> Date: Wed, 18 Jun 2025 18:40:19 +0700 Subject: [PATCH] feat: add ui for roadmap ui (#35) * fix: correct link for Roadmap navigation item * feat: add feature request and projects voting components * fix: correct spelling of 'Certificate' in ProjectsVote component * feat: add Request Feature popup with responsive dialog and drawer components * feat: enhance Header component with mobile menu animations and improved navigation styling * refactor: simplify Header component by removing unused scroll handling and improving mobile menu structure * fix: replace motion span with static span for header underline * fix: update FeatureRequestCTA text for clarity and adjust page layout margins * fix: remove DialogDescription from RequestFeaturePopup and update textarea placeholder for clarity * fix: remove DrawerDescription from RequestFeaturePopup for cleaner UI * fix: remove unused icon exports from icons * fix: replace XIcon with LuX in DialogClose for consistency * fix: remove unused icon export from index for cleaner module structure --- .../src/app/(public)/_components/header.tsx | 187 +++++--- .../app/(public)/_components/theme-toggle.tsx | 7 +- .../_components/feature-request-cta.tsx | 35 ++ .../roadmap/_components/projects-vote.tsx | 239 ++++++++++ .../_components/request-feature-popup.tsx | 120 +++++ .../landing/src/app/(public)/roadmap/page.tsx | 11 + .../src/app/(public)/roadmaps/page.tsx | 3 - .../app/(public)/{articles => teams}/page.tsx | 0 apps/landing/src/data/navigations.json | 6 +- apps/landing/src/hooks/use-media-query.ts | 33 ++ libs/shadcn-ui/package-lock.json | 434 ++++++++++++++++++ libs/shadcn-ui/package.json | 1 + libs/shadcn-ui/src/atoms/dialog.tsx | 142 ++++++ libs/shadcn-ui/src/atoms/drawer.tsx | 131 ++++++ libs/shadcn-ui/src/atoms/icons.tsx | 37 -- libs/shadcn-ui/src/atoms/index.ts | 4 +- libs/shadcn-ui/src/atoms/textarea.tsx | 18 + package-lock.json | 428 +++++++++++++++++ package.json | 1 + 19 files changed, 1728 insertions(+), 109 deletions(-) create mode 100644 apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx create mode 100644 apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx create mode 100644 apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx create mode 100644 apps/landing/src/app/(public)/roadmap/page.tsx delete mode 100644 apps/landing/src/app/(public)/roadmaps/page.tsx rename apps/landing/src/app/(public)/{articles => teams}/page.tsx (100%) create mode 100644 apps/landing/src/hooks/use-media-query.ts create mode 100644 libs/shadcn-ui/src/atoms/dialog.tsx create mode 100644 libs/shadcn-ui/src/atoms/drawer.tsx delete mode 100644 libs/shadcn-ui/src/atoms/icons.tsx create mode 100644 libs/shadcn-ui/src/atoms/textarea.tsx diff --git a/apps/landing/src/app/(public)/_components/header.tsx b/apps/landing/src/app/(public)/_components/header.tsx index 30b40d7..63aae9a 100644 --- a/apps/landing/src/app/(public)/_components/header.tsx +++ b/apps/landing/src/app/(public)/_components/header.tsx @@ -4,107 +4,170 @@ import { LogoSimple } from '@/app/_components/logo'; import NAVIGATIONS from '@/data/navigations.json'; import { Button } from '@components'; import { cn } from '@utils'; +import { AnimatePresence, motion } from 'framer-motion'; import Link from 'next/link'; -import { useRouter } from 'next/navigation'; +import { usePathname, useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { LuMenu, LuX } from 'react-icons/lu'; export function Header() { const router = useRouter(); - - const [isScrolled, setIsScrolled] = useState(false); + const pathname = usePathname(); const [mobileMenuOpen, setMobileMenuOpen] = useState(false); useEffect(() => { - const handleScroll = () => { - setIsScrolled(window.scrollY > 10); + if (mobileMenuOpen) { + document.body.style.overflow = 'hidden'; + } else { + document.body.style.overflow = 'auto'; + } + return () => { + document.body.style.overflow = 'auto'; }; - window.addEventListener('scroll', handleScroll); - return () => window.removeEventListener('scroll', handleScroll); - }, []); + }, [mobileMenuOpen]); + + useEffect(() => { + setMobileMenuOpen(false); + }, [pathname]); return ( -
-
-
-
- - - -
-
+
+
+ + + - {/* Desktop Navigation */} -
+
- - {/* Mobile Menu Button */} -
-
- {/* Mobile Menu */} - {mobileMenuOpen && ( -
- -
- )} + + + + + + )} + +
); } diff --git a/apps/landing/src/app/(public)/_components/theme-toggle.tsx b/apps/landing/src/app/(public)/_components/theme-toggle.tsx index 6eac564..f6f0feb 100644 --- a/apps/landing/src/app/(public)/_components/theme-toggle.tsx +++ b/apps/landing/src/app/(public)/_components/theme-toggle.tsx @@ -1,8 +1,9 @@ 'use client'; -import { Button, MoonIcon, SunIcon } from '@components'; +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(); @@ -28,9 +29,9 @@ export function ThemeToggle() { className="focus-visible:ring-0 cursor-pointer" > {theme === 'dark' ? ( - + ) : ( - + )} Toggle theme 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 new file mode 100644 index 0000000..e39d837 --- /dev/null +++ b/apps/landing/src/app/(public)/roadmap/_components/feature-request-cta.tsx @@ -0,0 +1,35 @@ +'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 new file mode 100644 index 0000000..fdc015f --- /dev/null +++ b/apps/landing/src/app/(public)/roadmap/_components/projects-vote.tsx @@ -0,0 +1,239 @@ +'use client'; + +import { Button, Card, CardContent, CardHeader, CardTitle } from '@components'; +import { AnimatePresence, motion } from 'framer-motion'; +import { useState } from 'react'; +import { BiUpvote } from 'react-icons/bi'; +import { FiCheckCircle } from 'react-icons/fi'; +import { MdOutlineOpenInNew } from 'react-icons/md'; + +export default function ProjectsVote() { + const [upcomingItems, setUpcomingItems] = useState([ + { + title: 'IMPHNEN Project Showcase', + description: 'Showcase projectmu ke member lain dan dapatkan feedback', + votes: 42, + voted: false, + }, + { + title: 'IMPHNEN Meme Generator', + description: 'Bikin meme kocak kapanpun dengan mudah', + votes: 42, + voted: false, + }, + ]); + + const inProgressItems = [ + { + title: 'IMPHNEN Twibbon', + description: 'Buat Twibbon kece untuk profile media sosialmu', + }, + { + title: 'IMPHNEN Certificate', + description: 'Cetak sertifikat keren secara instan untuk anggota IMPHNEN', + }, + ]; + + const completedItems = [ + { + title: 'IMPHNEN List Event', + description: + 'Koleksi daftar event dan kolaborasi seru yang bisa kamu ikuti', + }, + { + title: 'IMPHNEN Testimoni', + description: 'Berikan testimonial gokil buat komunitas IMPHNEN', + }, + { + title: 'IMPHNEN Roadmap by Vote', + description: 'Usulkan ide fitur seru dan ajak anggota lain buat voting', + }, + ]; + + const handleVote = (index: number) => { + const newItems = [...upcomingItems]; + newItems[index] = { + ...newItems[index], + votes: newItems[index].voted + ? newItems[index].votes - 1 + : newItems[index].votes + 1, + voted: !newItems[index].voted, + }; + setUpcomingItems(newItems); + }; + + 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 } }, + }; + + return ( +
+ {/* Vote Now Column */} +
+
+
+ πŸ”₯ +
+

Vote Now

+
+ + + + {upcomingItems.map((item, index) => ( + + + + + {item.title} + + + +

+ {item.description} +

+
+ handleVote(index)} + className={`flex items-center gap-2 px-4 py-2 rounded-lg text-sm font-medium transition-all ${ + item.voted + ? 'bg-primary-500 text-white hover:bg-primary-600' + : 'bg-gray-100 text-gray-700 hover:bg-gray-200' + }`} + > + + + + {item.voted ? 'Voted' : 'Vote'} + +
+ + + {item.votes} + +
+
+
+
+
+ ))} +
+
+
+ + {/* In Progress Column */} +
+
+
+ πŸ§‘β€πŸ”§ +
+

In Progress

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

{item.description}

+
+
+
+
+

+ {index === 0 ? 'Development started' : 'In development'} +

+
+
+
+
+ ))} +
+
+
+ + {/* Completed Column */} +
+
+
+ πŸ€“ +
+

Completed

+
+ + + + {completedItems.map((item, index) => ( + + + + + {item.title} + + + +

+ {item.description} +

+
+
+ + Implemented +
+ +
+
+
+
+ ))} +
+
+
+
+ ); +} 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 new file mode 100644 index 0000000..13dbfdf --- /dev/null +++ b/apps/landing/src/app/(public)/roadmap/_components/request-feature-popup.tsx @@ -0,0 +1,120 @@ +'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} + +
+