diff --git a/.github/workflows/deploy-landing.yml b/.github/workflows/deploy-landing.yml new file mode 100644 index 0000000..ce85078 --- /dev/null +++ b/.github/workflows/deploy-landing.yml @@ -0,0 +1,33 @@ +name: Deploy Landing + +on: + push: + paths: + - 'apps/landing/**' # INI BASED ON PATH CHANGES JADI NTAR KALAU ADA PUSH DIISNI OTOMATIS DEPLOY + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.3 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + port: ${{ secrets.SSH_PORT }} + script: | + set -e + if [ ! -d ~/imphnen-frontend-service ]; then + git clone https://github.com/IMPHNEN/imphnen-frontend-service.git ~/imphnen-frontend-service + else + cd ~/imphnen-frontend-service && git pull + fi + echo "NEXT_PUBLIC_API_URL=${{ secrets.NEXT_PUBLIC_API_URL }}" > ~/imphnen-frontend-service/apps/landing/.env + docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml down || true + docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml build + docker compose -f ~/imphnen-frontend-service/docker-compose-landing.yml up -d + docker image prune -af diff --git a/apps/dimentorin/public/image/mascot-character.webp b/apps/dimentorin/public/image/mascot-character.webp new file mode 100644 index 0000000..34267f5 Binary files /dev/null and b/apps/dimentorin/public/image/mascot-character.webp differ diff --git a/apps/dimentorin/public/image/mascot-full.webp b/apps/dimentorin/public/image/mascot-full.webp new file mode 100644 index 0000000..ccce7b6 Binary files /dev/null and b/apps/dimentorin/public/image/mascot-full.webp differ diff --git a/apps/dimentorin/public/image/mascot-text.webp b/apps/dimentorin/public/image/mascot-text.webp new file mode 100644 index 0000000..18d2edb Binary files /dev/null and b/apps/dimentorin/public/image/mascot-text.webp differ diff --git a/apps/dimentorin/public/image/testimonial.webp b/apps/dimentorin/public/image/testimonial.webp new file mode 100644 index 0000000..b4555c9 Binary files /dev/null and b/apps/dimentorin/public/image/testimonial.webp differ diff --git a/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp b/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp new file mode 100644 index 0000000..a6a07b6 Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/1-on-1-mentoring.webp differ diff --git a/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp b/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp new file mode 100644 index 0000000..e44f3cc Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/ai-powered-learning.webp differ diff --git a/apps/dimentorin/public/image/what-we-offer/community.webp b/apps/dimentorin/public/image/what-we-offer/community.webp new file mode 100644 index 0000000..f309286 Binary files /dev/null and b/apps/dimentorin/public/image/what-we-offer/community.webp differ diff --git a/apps/dimentorin/public/logos/logo.svg b/apps/dimentorin/public/logos/logo.svg new file mode 100644 index 0000000..ca0f36e --- /dev/null +++ b/apps/dimentorin/public/logos/logo.svg @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/apps/dimentorin/public/logos/simple.svg b/apps/dimentorin/public/logos/simple.svg new file mode 100644 index 0000000..0fd70f3 --- /dev/null +++ b/apps/dimentorin/public/logos/simple.svg @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx new file mode 100644 index 0000000..65e0f6b --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/_components/cta-section.tsx @@ -0,0 +1,154 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms'; +import { cn } from '@imphnen-frontend-service/utils'; +import { FC, useRef } from 'react'; +import { motion, useInView, Variants } from 'framer-motion'; + +export const CTASection: FC = () => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, amount: 0.2 }) + + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, + } + + const scaleVariant: Variants = { + hidden: { scale: 0 }, + visible: { + scale: 1, + transition: { + duration: 0.5, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + const contentVariant: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + return ( +
+ + {/* Background gradients and shapes */} +
+ + + + + + +
+
+ + + +
+ + Start Your IT Journey Now! + + + Jangan biarkan progress mu hanya jadi side story! + + + Dapatkan mentor terbaik dan gunakan AI untuk belajar lebih cepat. Waktunya jadi protagonist dalam perjalanan IT-mu! + +
+ + + + + +
+
+ ) +} diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx new file mode 100644 index 0000000..3b2e480 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/_components/faq-section.tsx @@ -0,0 +1,114 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms' +import { Accordion, AccordionOptions } from '@imphnen-frontend-service/ui/molecules' +import { FC, useRef } from 'react' +import { motion, useInView, Variants } from 'framer-motion' +import { cn, For } from '@imphnen-frontend-service/utils' + +const FAQ_DATA: AccordionOptions[] = [ + { title: 'Apakah saya bisa memilih mentor sendiri?', description: 'Tentu! Kamu bisa memilih mentor yang sesuai dengan kebutuhan dan level skill kamu.' }, + { title: 'Apakah mentoring ini cocok untuk pemula?', description: 'Yap! Baik newbie maupun seasoned dev bisa menemukan mentor yang pas untuk mempercepat progres belajarnya.' }, + { title: 'Apakah ini seperti sekolah coding biasa?', description: 'Nope! Ini lebih dari sekadar kursus-kursus AI guide dan mentor berpengalaman, kamu mendapatkan pengalaman belajar yang jauh lebih personal dan efektif.' }, +] + +export const FAQSection: FC = () => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, amount: 0.2 }) + + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, + } + + const childVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + return ( +
+ {/* Background shapes */} +
+ + + +
+ + + + + +
+ + Console log for new developers + + + + + {(props) => ( + + + + )} + + +
+
+ ) +} diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx new file mode 100644 index 0000000..06437ee --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/_components/hero-section.tsx @@ -0,0 +1,73 @@ +import { Button } from "@imphnen-frontend-service/ui/atoms"; +import { motion } from 'framer-motion' + +export function HeroSection() { + return ( +
+ {/* Background gradients and shapes */} +
+ + + + + + + +
+
+
+ +
+ +

DIMENTORIN

+

+ Learn IT smarter with AI & expert mentors! +

+

+ Apakah kamu siap untuk grind skill TI-mu ke level Ultimate? Dengan AI sebagai navigator dan mentor profesional sebagai sensei-mu, perjalanan belajarmu akan lebih cepat, efektif, dan penuh EXP! +

+
+ + + + +
+
+ ) +} \ No newline at end of file diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx new file mode 100644 index 0000000..3ead980 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/_components/testimonial-section.tsx @@ -0,0 +1,113 @@ +import { Button } from '@imphnen-frontend-service/ui/atoms' +import { For } from '@imphnen-frontend-service/utils' +import { FC, useRef } from 'react' +import { motion, useInView, Variants } from 'framer-motion' +import { StarFilled } from '@ant-design/icons' + +export const TestimonialSection: FC = () => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, amount: 0.2 }) + + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, + } + + const childVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + return ( +
+ + + + + + Words from our dellow devs + + +
+ + + {(_, index) => ( + +
+ Testimonial +
+
+

+ Riko, Junior Web Developer +

+

+ Rasanya seperti punya AI waifu yang ngajarin ngoding! Mentoringnya juga super insightful. Thanks, Dimentorin! +

+ + + +
+
+ )} +
+
+
+
+ ) +} diff --git a/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx b/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx new file mode 100644 index 0000000..1bf434e --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/_components/what-we-offer-section.tsx @@ -0,0 +1,84 @@ +import { Button } from "@imphnen-frontend-service/ui/atoms"; +import { For } from "@imphnen-frontend-service/utils"; +import { FC, useRef } from "react"; +import { motion, useInView, Variants } from 'framer-motion' + +const OFFERS: { title: string; description: string; image: string }[] = [ + { title: "AI-Powered Learning", description: "Level Up Instantly!", image: "/image/what-we-offer/ai-powered-learning.webp" }, + { title: "1-on-1 Mentoring", description: "Sensei dari Dunia Nyata!", image: "/image/what-we-offer/1-on-1-mentoring.webp" }, + { title: "Community & Resources", description: "Connect dengan Fellow Devs!", image: "/image/what-we-offer/community.webp" }, +] + +export const WhatWeOfferSection: FC = () => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, amount: 0.2 }) + + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.2, + }, + }, + } + + const childVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + return ( +
+ + + + + + + {(offer) => ( + + {offer.title} +
+

{offer.title}

+

{offer.description}

+
+
+ )} +
+
+
+ ) +} \ No newline at end of file diff --git a/apps/dimentorin/src/app/(public)/(home)/page.tsx b/apps/dimentorin/src/app/(public)/(home)/page.tsx new file mode 100644 index 0000000..87b0dee --- /dev/null +++ b/apps/dimentorin/src/app/(public)/(home)/page.tsx @@ -0,0 +1,20 @@ +import { FC, ReactElement } from 'react'; +import { HeroSection } from './_components/hero-section'; +import { WhatWeOfferSection } from './_components/what-we-offer-section'; +import { TestimonialSection } from './_components/testimonial-section'; +import { FAQSection } from './_components/faq-section'; +import { CTASection } from './_components/cta-section'; + +export const Components: FC = (): ReactElement => { + return ( + <> + + + + + + + ); +}; + +export default Components; diff --git a/apps/dimentorin/src/app/(public)/_components/footer.tsx b/apps/dimentorin/src/app/(public)/_components/footer.tsx new file mode 100644 index 0000000..daf2a1f --- /dev/null +++ b/apps/dimentorin/src/app/(public)/_components/footer.tsx @@ -0,0 +1,157 @@ +import { DiscordOutlined, GithubOutlined, InstagramOutlined } from '@ant-design/icons'; +import { cn, For } from '@imphnen-frontend-service/utils'; +import React, { FC, useRef } from 'react'; +import { Link } from 'react-router-dom'; +import { motion, useInView, Variants } from 'framer-motion'; +import { SteamIcon } from '../../_components/icons'; + +const PAGES: { label: string; href: string }[] = [ + { label: "Home", href: "/" }, + { label: 'Mentoring', href: '/mentoring' }, + { label: 'Resources', href: '/resources' }, + { label: 'Articles', href: '/articles' }, +] + +const COMMUNITY: { label: string; href: string; icon: React.ReactNode }[] = [ + { label: 'Discord', href: 'https://discord.gg/imphnen', icon: }, + { label: 'Steam', href: 'https://steamcommunity.com/groups/IMPHNEN', icon: }, + { label: 'Github', href: 'https://github.com/IMPHNEN', icon: }, + { label: 'Instagram', href: 'https://www.instagram.com/imphnen.dev', icon: }, +] + +export const Footer: FC = () => { + const ref = useRef(null) + const isInView = useInView(ref, { once: true, amount: 0.2 }) + + const containerVariants: Variants = { + hidden: { opacity: 0 }, + visible: { opacity: 1 }, + } + + const childVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + const mascotVariants: Variants = { + hidden: { opacity: 0, y: 20, rotate: -12 }, + visible: { + opacity: 1, + y: 0, + rotate: 0, + transition: { + duration: 0.5, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + const mascotTextVariants: Variants = { + hidden: { opacity: 0, y: 20 }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + delay: 0.4, + ease: [0.25, 0.46, 0.45, 0.94], + }, + }, + } + + return ( +
+ + {/* Mascot */} + + + + + +
+
+ + + + Dimentorin by IMPHNEN + + + Karena Belajar IT Itu Harusnya Se-Seru Anime Favoritmu! + +
+ + + © IMPHNEN {new Date().getFullYear()} All Rights Reserved + +
+ + +
+

Pages

+
    + + {({ label, href }) => ( +
  • + {label} +
  • + )} +
    +
+
+ +
+

Join Our Community

+
    + + {({ label, href, icon }) => ( +
  • + + {icon} + {label} + +
  • + )} +
    +
+
+
+ + + © IMPHNEN {new Date().getFullYear()} All Rights Reserved + +
+
+ ) +} diff --git a/apps/dimentorin/src/app/(public)/_components/header.tsx b/apps/dimentorin/src/app/(public)/_components/header.tsx new file mode 100644 index 0000000..585e126 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/_components/header.tsx @@ -0,0 +1,93 @@ +import { CloseOutlined, MenuOutlined } from "@ant-design/icons"; +import { Button } from "@imphnen-frontend-service/ui/atoms"; +import { cn, For, Show } from "@imphnen-frontend-service/utils"; +import { motion, useMotionValueEvent, useScroll, Variants } from "framer-motion"; +import { FC, useState } from "react"; +import { Link, useLocation } from "react-router-dom"; + +const MENUS: { label: string; href: string }[] = [ + { label: "Home", href: "/" }, + { label: 'Mentoring', href: '/mentoring' }, + { label: 'Resources', href: '/resources' }, + { label: 'Articles', href: '/articles' }, +] + +export const Header: FC = () => { + const location = useLocation(); + const { scrollY } = useScroll() + + const [expandMenu, setExpandMenu] = useState(false); + const [show, setShow] = useState(true) + + const headerVariants: Variants = { + hidden: { opacity: 0, y: -100 }, + show: { opacity: 1, y: 0 }, + }; + + useMotionValueEvent(scrollY, "change", (latest) => { + const prev = scrollY.getPrevious() || 0 + if (latest > prev && latest > 100) setShow(false) + else setShow(true) + }) + + return ( +
+ +
+
+ IMPHNEN Logo +
+ + + +
+ + +
+
+
+
+ ); +} diff --git a/apps/dimentorin/src/app/(public)/layout.tsx b/apps/dimentorin/src/app/(public)/layout.tsx new file mode 100644 index 0000000..d9ffaf8 --- /dev/null +++ b/apps/dimentorin/src/app/(public)/layout.tsx @@ -0,0 +1,15 @@ +import { Outlet } from "react-router-dom"; +import { Header } from "./_components/header"; +import { Footer } from "./_components/footer"; + +export default function Layout() { + return ( +
+
+
+ +
+
+
+ ); +} diff --git a/apps/dimentorin/src/app/_components/icons/index.ts b/apps/dimentorin/src/app/_components/icons/index.ts new file mode 100644 index 0000000..342889c --- /dev/null +++ b/apps/dimentorin/src/app/_components/icons/index.ts @@ -0,0 +1 @@ +export * from './steam'; \ No newline at end of file diff --git a/apps/dimentorin/src/app/_components/icons/steam.tsx b/apps/dimentorin/src/app/_components/icons/steam.tsx new file mode 100644 index 0000000..386439b --- /dev/null +++ b/apps/dimentorin/src/app/_components/icons/steam.tsx @@ -0,0 +1,9 @@ +import { FC } from "react" + +export const SteamIcon: FC> = ({ ...props }) => { + return ( + + + + ) +} diff --git a/apps/dimentorin/src/app/_hooks/use-resend-otp.ts b/apps/dimentorin/src/app/_hooks/use-resend-otp.ts new file mode 100644 index 0000000..29f0b71 --- /dev/null +++ b/apps/dimentorin/src/app/_hooks/use-resend-otp.ts @@ -0,0 +1,10 @@ +import { useSendOTP } from '@imphnen-frontend-service/utils'; + +export const useResendOtpHook = () => { + + const { resendOTP, isLoading } = useSendOTP(); + + return { + resendOTP + }; +}; \ No newline at end of file diff --git a/apps/dimentorin/src/app/auth/register/otp/page.tsx b/apps/dimentorin/src/app/auth/register/otp/page.tsx index 249ca78..a79b525 100644 --- a/apps/dimentorin/src/app/auth/register/otp/page.tsx +++ b/apps/dimentorin/src/app/auth/register/otp/page.tsx @@ -1,12 +1,38 @@ -import { FC, ReactElement } from 'react'; +import { FC, ReactElement, useEffect, useState } from 'react'; import { ControlledInputField, RegisterResetBanner } from "@imphnen-frontend-service/ui/organisms"; import { useOtpHook } from '../../../_hooks/use-otp'; import { Button } from '@imphnen-frontend-service/ui/atoms'; import { useSearchParams } from 'react-router-dom'; +import { useResendOtpHook } from '../../../_hooks/use-resend-otp'; export const Components: FC = (): ReactElement => { const { form, onSubmit, isLoading } = useOtpHook() const [ searchParams ] = useSearchParams() + const [ disabled, setDisabled] = useState(true); + const [ timeLeft, setTimeLeft ] = useState(5 * 60); + const { resendOTP } = useResendOtpHook() + + useEffect(() => { + if (disabled) { + const interval = setInterval(() => { + setTimeLeft(prev => { + if (prev <= 1) { + clearInterval(interval); + setDisabled(false); + return 0; + } + return prev - 1; + }); + }, 1000); + return () => clearInterval(interval); + } + }, [disabled]); + + const formatTime = (seconds: number) => { + const m = Math.floor(seconds / 60); + const s = seconds % 60; + return `${m}:${s.toString().padStart(2, '0')}`; + }; return (
@@ -26,6 +52,13 @@ export const Components: FC = (): ReactElement => { maxLength={6} control={form.control} /> +
diff --git a/apps/dimentorin/src/app/page.tsx b/apps/dimentorin/src/app/page.tsx deleted file mode 100644 index f135208..0000000 --- a/apps/dimentorin/src/app/page.tsx +++ /dev/null @@ -1,7 +0,0 @@ -import { FC, ReactElement } from 'react'; - -export const Components: FC = (): ReactElement => { - return <>Hallo; -}; - -export default Components; diff --git a/apps/dimentorin/src/index.css b/apps/dimentorin/src/index.css index ab312f2..931d17d 100644 --- a/apps/dimentorin/src/index.css +++ b/apps/dimentorin/src/index.css @@ -119,3 +119,9 @@ font-weight: 400; } } + +@layer utilities { + .scrollbar-hide::-webkit-scrollbar { + @apply hidden; + } +} diff --git a/apps/dimentorin/src/middleware.ts b/apps/dimentorin/src/middleware.ts index c172d70..c9e06f7 100644 --- a/apps/dimentorin/src/middleware.ts +++ b/apps/dimentorin/src/middleware.ts @@ -10,7 +10,7 @@ const mappingPublicRoutes = [ '/auth/login', '/auth/forgot', '/auth/forgot/otp', - '/auth/register', + '/auth/register', '/auth/register/otp', '/auth/register/success', '/auth/new-password', diff --git a/apps/landing/next.config.js b/apps/landing/next.config.js index 46c7617..6ba3010 100644 --- a/apps/landing/next.config.js +++ b/apps/landing/next.config.js @@ -1,8 +1,10 @@ const { composePlugins, withNx } = require('@nx/next'); +const path = require('path'); /** @type {import('@nx/next/plugins/with-nx').WithNxOptions} */ const nextConfig = { nx: { svgr: false }, + output: 'standalone', }; module.exports = composePlugins(withNx)(nextConfig); 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} + +
+