feat: make header components atomic & remove unused menu
This commit is contained in:
@@ -1,159 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
MenuIcon,
|
|
||||||
XIcon,
|
|
||||||
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
|
||||||
import { cn } from '@imphnen-frontend-service/utils';
|
|
||||||
import Image from 'next/image';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { SimpleThemeToggle } from './simple-theme-toggle';
|
|
||||||
|
|
||||||
export function Header() {
|
|
||||||
const [isScrolled, setIsScrolled] = useState(false);
|
|
||||||
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const handleScroll = () => {
|
|
||||||
setIsScrolled(window.scrollY > 10);
|
|
||||||
};
|
|
||||||
window.addEventListener('scroll', handleScroll);
|
|
||||||
return () => window.removeEventListener('scroll', handleScroll);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<header
|
|
||||||
className={cn(
|
|
||||||
'sticky top-0 z-50 w-full transition-all duration-300',
|
|
||||||
isScrolled
|
|
||||||
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
|
|
||||||
: 'bg-transparent'
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div className="container flex h-16 items-center justify-between">
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<div className="relative overflow-hidden rounded">
|
|
||||||
<Link href="/">
|
|
||||||
<Image
|
|
||||||
src="/logo.png"
|
|
||||||
alt="IMPHNEN"
|
|
||||||
width={64}
|
|
||||||
height={64}
|
|
||||||
className="object-cover"
|
|
||||||
/>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Desktop Navigation */}
|
|
||||||
<nav className="hidden md:flex items-center gap-8">
|
|
||||||
<Link href="#fitur" className="text-sm font-medium relative group">
|
|
||||||
<span className="transition-colors hover:text-primary">Fitur</span>
|
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#komunitas"
|
|
||||||
className="text-sm font-medium relative group"
|
|
||||||
>
|
|
||||||
<span className="transition-colors hover:text-primary">
|
|
||||||
Komunitas
|
|
||||||
</span>
|
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#sumber-belajar"
|
|
||||||
className="text-sm font-medium relative group"
|
|
||||||
>
|
|
||||||
<span className="transition-colors hover:text-primary">
|
|
||||||
Sumber Belajar
|
|
||||||
</span>
|
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#testimoni"
|
|
||||||
className="text-sm font-medium relative group"
|
|
||||||
>
|
|
||||||
<span className="transition-colors hover:text-primary">
|
|
||||||
Testimoni
|
|
||||||
</span>
|
|
||||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
|
||||||
</Link>
|
|
||||||
</nav>
|
|
||||||
|
|
||||||
<div className="flex items-center gap-4">
|
|
||||||
<SimpleThemeToggle />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="hidden md:flex bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 transition-all duration-300 font-bold text-black hover:text-white cursor-pointer"
|
|
||||||
onClick={() =>
|
|
||||||
window.open('https://discord.com/invite/imphnen', '_blank')
|
|
||||||
}
|
|
||||||
>
|
|
||||||
Gabung Discord
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
{/* Mobile Menu Button */}
|
|
||||||
<Button
|
|
||||||
variant="ghost"
|
|
||||||
size="icon"
|
|
||||||
className="md:hidden"
|
|
||||||
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
|
|
||||||
>
|
|
||||||
{mobileMenuOpen ? (
|
|
||||||
<XIcon className="h-6 w-6" />
|
|
||||||
) : (
|
|
||||||
<MenuIcon className="h-6 w-6" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Mobile Menu */}
|
|
||||||
{mobileMenuOpen && (
|
|
||||||
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
|
|
||||||
<nav className="container flex flex-col py-4 text-center">
|
|
||||||
<Link
|
|
||||||
href="#fitur"
|
|
||||||
className="py-3 text-sm font-medium border-b border-border/50"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
Fitur
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#komunitas"
|
|
||||||
className="py-3 text-sm font-medium border-b border-border/50"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
Komunitas
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#sumber-belajar"
|
|
||||||
className="py-3 text-sm font-medium border-b border-border/50"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
Sumber Belajar
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="#testimoni"
|
|
||||||
className="py-3 text-sm font-medium"
|
|
||||||
onClick={() => setMobileMenuOpen(false)}
|
|
||||||
>
|
|
||||||
Testimoni
|
|
||||||
</Link>
|
|
||||||
<a
|
|
||||||
href="https://discord.com/invite/imphnen"
|
|
||||||
target="_blank"
|
|
||||||
className="mt-4"
|
|
||||||
>
|
|
||||||
<Button className="mt-4 bg-gradient-to-r from-primary to-blue-400 hover:from-primary/90 hover:to-blue-400/90 font-bold text-black hover:text-white cursor-pointer">
|
|
||||||
Gabung Discord
|
|
||||||
</Button>
|
|
||||||
</a>
|
|
||||||
</nav>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</header>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function DesktopNavigation() {
|
||||||
|
return (
|
||||||
|
<nav className="hidden md:flex items-center gap-8">
|
||||||
|
<Link href="/about" className="text-sm font-medium relative group">
|
||||||
|
<span className="transition-colors hover:text-primary">About</span>
|
||||||
|
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/events" className="text-sm font-medium relative group">
|
||||||
|
<span className="transition-colors hover:text-primary">Event</span>
|
||||||
|
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||||
|
</Link>
|
||||||
|
<Link href="/testimonials" className="text-sm font-medium relative group">
|
||||||
|
<span className="transition-colors hover:text-primary">
|
||||||
|
Testimonial
|
||||||
|
</span>
|
||||||
|
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { cn } from '@imphnen-frontend-service/utils';
|
||||||
|
import { ReactNode, useEffect, useState } from 'react';
|
||||||
|
|
||||||
|
export function HeaderWrapper({ children }: { children: ReactNode }) {
|
||||||
|
const [isScrolled, setIsScrolled] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleScroll = () => {
|
||||||
|
setIsScrolled(window.scrollY > 10);
|
||||||
|
};
|
||||||
|
window.addEventListener('scroll', handleScroll);
|
||||||
|
return () => window.removeEventListener('scroll', handleScroll);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<header
|
||||||
|
className={cn(
|
||||||
|
'sticky top-0 z-50 w-full transition-all duration-300',
|
||||||
|
isScrolled
|
||||||
|
? 'bg-background/80 backdrop-blur-md border-b shadow-sm'
|
||||||
|
: 'bg-transparent'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useMobileMenuStore } from '../../../../stores/mobile-menu-store';
|
||||||
|
import { DesktopNavigation } from './desktop-navigation';
|
||||||
|
import { HeaderWrapper } from './header-wrapper';
|
||||||
|
import { Logo } from './logo';
|
||||||
|
import { MobileMenuHamburger } from './mobile-menu-hamburger';
|
||||||
|
import { MobileNavigation } from './mobile-navigation';
|
||||||
|
import { SimpleThemeToggle } from './simple-theme-toggle';
|
||||||
|
|
||||||
|
export function Header() {
|
||||||
|
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HeaderWrapper>
|
||||||
|
<div className="container flex h-16 items-center justify-between">
|
||||||
|
<Logo />
|
||||||
|
|
||||||
|
<DesktopNavigation />
|
||||||
|
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<SimpleThemeToggle />
|
||||||
|
<MobileMenuHamburger />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{mobileMenuOpen && <MobileNavigation />}
|
||||||
|
</HeaderWrapper>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import Image from 'next/image';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function Logo() {
|
||||||
|
return (
|
||||||
|
<Link href="/">
|
||||||
|
<Image
|
||||||
|
src="/logo.png"
|
||||||
|
alt="IMPHNEN"
|
||||||
|
width={64}
|
||||||
|
height={64}
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
MenuIcon,
|
||||||
|
XIcon,
|
||||||
|
} from '@imphnen-frontend-service/shadcn-ui/atoms';
|
||||||
|
import { useMobileMenuStore } from '../../../../stores/mobile-menu-store';
|
||||||
|
|
||||||
|
export function MobileMenuHamburger() {
|
||||||
|
const mobileMenuOpen = useMobileMenuStore((s) => s.mobileMenuOpen);
|
||||||
|
const toggleMobileMenu = useMobileMenuStore((s) => s.toggleMobileMenu);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="md:hidden"
|
||||||
|
onClick={toggleMobileMenu}
|
||||||
|
>
|
||||||
|
{mobileMenuOpen ? (
|
||||||
|
<XIcon className="h-6 w-6" />
|
||||||
|
) : (
|
||||||
|
<MenuIcon className="h-6 w-6" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
export function MobileNavigation() {
|
||||||
|
return (
|
||||||
|
<div className="md:hidden border-t bg-background/95 backdrop-blur-md">
|
||||||
|
<nav className="container flex flex-col py-4 text-center">
|
||||||
|
<Link
|
||||||
|
href="/about"
|
||||||
|
className="py-3 text-sm font-medium border-b border-border/50"
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/events"
|
||||||
|
className="py-3 text-sm font-medium border-b border-border/50"
|
||||||
|
>
|
||||||
|
Event
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/testimonias"
|
||||||
|
className="py-3 text-sm font-medium border-b border-border/50"
|
||||||
|
>
|
||||||
|
Testimonial
|
||||||
|
</Link>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -8,8 +8,9 @@ import { ThemeProvider } from './_components/theme-provider';
|
|||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'IMPHNEN - Ingin Menjadi Programmer Handal?',
|
title: 'IMPHNEN - Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
|
||||||
description: 'Komunitas belajar programming untuk semua level',
|
description:
|
||||||
|
'Komunitas Ingin Menjadi Programmer Handal Namung Enggan Ngonding',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|||||||
@@ -1,41 +1,27 @@
|
|||||||
import { getGlobalsCallToActionSection } from '../../services/get-globals-call-to-action-section';
|
import { getGlobalsCallToActionSection } from '../../services/get-globals-call-to-action-section';
|
||||||
import { getGlobalsCommunitiesSection } from '../../services/get-globals-communitues-section';
|
import { getGlobalsCommunitiesSection } from '../../services/get-globals-communitues-section';
|
||||||
import { getGlobalsFeaturesSection } from '../../services/get-globals-features-section';
|
|
||||||
import { getGlobalsHeroSection } from '../../services/get-globals-hero-section';
|
import { getGlobalsHeroSection } from '../../services/get-globals-hero-section';
|
||||||
import { getGlobalsLearningResourcesSection } from '../../services/get-globals-learning-resources-section';
|
|
||||||
import { getGlobalsTestimonialsSection } from '../../services/get-globals-testimonials-section';
|
import { getGlobalsTestimonialsSection } from '../../services/get-globals-testimonials-section';
|
||||||
import { CallToAction } from './_components/call-to-action';
|
import { CallToAction } from './_components/call-to-action';
|
||||||
import { Community } from './_components/community';
|
import { Community } from './_components/community';
|
||||||
import { Features } from './_components/features';
|
|
||||||
import { Hero } from './_components/hero';
|
import { Hero } from './_components/hero';
|
||||||
import { LearningResources } from './_components/learning-resources';
|
|
||||||
import { Testimonials } from './_components/testimonials';
|
import { Testimonials } from './_components/testimonials';
|
||||||
|
|
||||||
export const revalidate = 10; // Seconds
|
export const revalidate = 10; // Seconds
|
||||||
|
|
||||||
export default async function Page() {
|
export default async function Page() {
|
||||||
const [
|
const [heroData, communitiesData, testimonialsData, callToActionData] =
|
||||||
heroData,
|
await Promise.all([
|
||||||
featuresData,
|
getGlobalsHeroSection(),
|
||||||
communitiesData,
|
getGlobalsCommunitiesSection(),
|
||||||
learningResourcesData,
|
getGlobalsTestimonialsSection(),
|
||||||
testimonialsData,
|
getGlobalsCallToActionSection(),
|
||||||
callToActionData,
|
]);
|
||||||
] = await Promise.all([
|
|
||||||
getGlobalsHeroSection(),
|
|
||||||
getGlobalsFeaturesSection(),
|
|
||||||
getGlobalsCommunitiesSection(),
|
|
||||||
getGlobalsLearningResourcesSection(),
|
|
||||||
getGlobalsTestimonialsSection(),
|
|
||||||
getGlobalsCallToActionSection(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Hero {...heroData} />
|
<Hero {...heroData} />
|
||||||
<Features {...featuresData} />
|
|
||||||
<Community {...communitiesData} />
|
<Community {...communitiesData} />
|
||||||
<LearningResources {...learningResourcesData} />
|
|
||||||
<Testimonials {...testimonialsData} />
|
<Testimonials {...testimonialsData} />
|
||||||
<CallToAction {...callToActionData} />
|
<CallToAction {...callToActionData} />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
|
||||||
|
interface MobileMenuState {
|
||||||
|
mobileMenuOpen: boolean;
|
||||||
|
toggleMobileMenu: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMobileMenuStore = create<MobileMenuState>((set) => ({
|
||||||
|
mobileMenuOpen: false,
|
||||||
|
toggleMobileMenu: () =>
|
||||||
|
set((state) => ({ mobileMenuOpen: !state.mobileMenuOpen })),
|
||||||
|
}));
|
||||||
Generated
+31
-1
@@ -38,7 +38,8 @@
|
|||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2",
|
||||||
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.5",
|
"@babel/core": "^7.14.5",
|
||||||
@@ -40387,6 +40388,35 @@
|
|||||||
"url": "https://github.com/sponsors/colinhacks"
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zustand": {
|
||||||
|
"version": "5.0.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.4.tgz",
|
||||||
|
"integrity": "sha512-39VFTN5InDtMd28ZhjLyuTnlytDr9HfwO512Ai4I8ZABCoyAj4F1+sr7sD1jP/+p7k77Iko0Pb5NhgBFDCX0kQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=12.20.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@types/react": ">=18.0.0",
|
||||||
|
"immer": ">=9.0.6",
|
||||||
|
"react": ">=18.0.0",
|
||||||
|
"use-sync-external-store": ">=1.2.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"@types/react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"immer": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"use-sync-external-store": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/zwitch": {
|
"node_modules/zwitch": {
|
||||||
"version": "2.0.4",
|
"version": "2.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz",
|
||||||
|
|||||||
+2
-1
@@ -55,7 +55,8 @@
|
|||||||
"sharp": "^0.34.1",
|
"sharp": "^0.34.1",
|
||||||
"sonner": "^2.0.3",
|
"sonner": "^2.0.3",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2",
|
||||||
|
"zustand": "^5.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.14.5",
|
"@babel/core": "^7.14.5",
|
||||||
|
|||||||
Reference in New Issue
Block a user