diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..d376401 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": false, + "tailwind": { + "config": "tailwind.config.js", + "css": "src/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "iconLibrary": "lucide", + "rtl": false, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "registries": {} +} diff --git a/frontend/jsconfig.json b/frontend/jsconfig.json new file mode 100644 index 0000000..3255bae --- /dev/null +++ b/frontend/jsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "baseUrl": ".", + "paths": { + "@/*": [ + "./src/*" + ] + } + } +} \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index f6e01de..9dc8f8f 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,12 +9,18 @@ "version": "0.0.0", "dependencies": { "axios": "^1.13.5", - "framer-motion": "^10.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^10.18.0", + "lucide-react": "^0.564.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", - "tailwindcss": "^3.4.19" + "tailwind-merge": "^3.4.1", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.1" }, "devDependencies": { "@eslint/js": "^9.39.1", @@ -1862,6 +1868,27 @@ "node": ">= 6" } }, + "node_modules/class-variance-authority": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", + "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", + "license": "Apache-2.0", + "dependencies": { + "clsx": "^2.1.1" + }, + "funding": { + "url": "https://polar.sh/cva" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -2948,6 +2975,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2977,6 +3010,15 @@ "yallist": "^3.0.2" } }, + "node_modules/lucide-react": { + "version": "0.564.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.564.0.tgz", + "integrity": "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -3800,6 +3842,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tailwind-merge": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.1.tgz", + "integrity": "sha512-2OA0rFqWOkITEAOFWSBSApYkDeH9t2B3XSJuI4YztKBzK3mX0737A2qtxDZ7xkw9Zfh0bWl+r34sF3HXV+Ig7Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/dcastil" + } + }, "node_modules/tailwindcss": { "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", @@ -3837,6 +3889,15 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-animate": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/tailwindcss-animate/-/tailwindcss-animate-1.0.7.tgz", + "integrity": "sha512-bl6mpH3T7I3UFxuvDEXLxy/VuFxBk5bbzplh7tXI68mwMokNYd1t9qPBHlnyTwfa4JGC4zP516I1hYYtQ/vspA==", + "license": "MIT", + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -3952,6 +4013,21 @@ "punycode": "^2.1.0" } }, + "node_modules/usehooks-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/usehooks-ts/-/usehooks-ts-3.1.1.tgz", + "integrity": "sha512-I4diPp9Cq6ieSUH2wu+fDAVQO43xwtulo+fKEidHUwZPnYImbtkTjzIJYcDcJqxgmX31GVqNFURodvcgHcW0pA==", + "license": "MIT", + "dependencies": { + "lodash.debounce": "^4.0.8" + }, + "engines": { + "node": ">=16.15.0" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 381582f..c4aeb24 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,12 +11,18 @@ }, "dependencies": { "axios": "^1.13.5", - "framer-motion": "^10.0.1", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "framer-motion": "^10.18.0", + "lucide-react": "^0.564.0", "react": "^18.2.0", "react-dom": "^18.2.0", "react-icons": "^5.5.0", "react-router-dom": "^6.8.1", - "tailwindcss": "^3.4.19" + "tailwind-merge": "^3.4.1", + "tailwindcss": "^3.4.19", + "tailwindcss-animate": "^1.0.7", + "usehooks-ts": "^3.1.1" }, "devDependencies": { "@eslint/js": "^9.39.1", diff --git a/frontend/public/gambar_header.png b/frontend/public/gambar_header.png new file mode 100644 index 0000000..9a2dba7 Binary files /dev/null and b/frontend/public/gambar_header.png differ diff --git a/frontend/public/logo_polije.png b/frontend/public/logo_polije.png new file mode 100644 index 0000000..637fd70 Binary files /dev/null and b/frontend/public/logo_polije.png differ diff --git a/frontend/public/logo_polijecare.png b/frontend/public/logo_polijecare.png index f1eecd6..f16df76 100644 Binary files a/frontend/public/logo_polijecare.png and b/frontend/public/logo_polijecare.png differ diff --git a/frontend/src/components/Hero.jsx b/frontend/src/components/Hero.jsx index cd86fbd..0194702 100644 --- a/frontend/src/components/Hero.jsx +++ b/frontend/src/components/Hero.jsx @@ -13,32 +13,32 @@ const Hero = ({ heroData }) => { const hero = heroData || defaultHero; return ( -
{/* Background Decorations */} -
- + - {
{/* Left Content */} - - - { > {hero.title} - - { - { {hero.description} - { > Butuh Bantuan Darurat - + { whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }} > - @@ -134,7 +134,7 @@ const Hero = ({ heroData }) => { {/* Trust Indicators */} - {
100% Rahasia
- +
Profesional
- +
24/7 Support @@ -159,7 +159,7 @@ const Hero = ({ heroData }) => { {/* Right Content - Logo & Branding */} - { transition={{ duration: 0.8, delay: 0.3 }} > {/* Main Logo Container */} - -
-
- {/* Logo Image */} - -
- Polijecare Logo -
- {/* Glow Effect */} -
-
- - {/* Brand Text */} - -

Polijecare

-

Satgas PPKPT Polije

-
-
-
-
+ header gambar
- {/* Background Shape */} -
+ {/* Background Shape */} +
{/* Scroll Indicator */} - { const [isScrolled, setIsScrolled] = useState(false); + const [activeLink, setActiveLink] = useState('#hero'); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isLoginModalOpen, setIsLoginModalOpen] = useState(false); const { isAuthenticated, user, logout } = useAuth(); @@ -22,14 +26,56 @@ const Navbar = () => { { name: 'Kontak', href: '#contact' } ]; + const tabs = [ + { title: "Beranda", icon: Home }, + { title: "Tentang Kami", icon: Info }, + { title: "Cara Melapor", icon: FileText }, + { title: "Artikel", icon: BookOpen }, + { title: "Kontak", icon: Phone }, + ]; + + const handleNavClick = (href) => { + if (href.startsWith('#')) { + const element = document.querySelector(href); + if (element) { + element.scrollIntoView({ behavior: 'smooth' }); + } + } + setIsMobileMenuOpen(false); + }; + + + useEffect(() => { const handleScroll = () => { - setIsScrolled(window.scrollY > 20); + // Logic for switching navbar type (Standard vs Expandable) + // Switch when approaching 'about' section or scrolled past 1st screen + const aboutSection = document.getElementById('about'); + const threshold = aboutSection ? aboutSection.offsetTop - 400 : window.innerHeight - 200; + setIsScrolled(window.scrollY > threshold); + + // ScrollSpy Logic + const sections = navLinks.map(link => link.href.substring(1)); + let currentSection = ""; + + for (const section of sections) { + const element = document.getElementById(section); + if (element) { + const rect = element.getBoundingClientRect(); + if (rect.top <= 150 && rect.bottom >= 150) { + currentSection = "#" + section; + } + } + } + + if (currentSection && currentSection !== activeLink) { + setActiveLink(currentSection); + } }; window.addEventListener('scroll', handleScroll); return () => window.removeEventListener('scroll', handleScroll); - }, []); + }, [navLinks, activeLink]); const handleLogout = () => { logout(); @@ -38,7 +84,7 @@ const Navbar = () => { const handleDashboardRedirect = () => { if (!user) return; - + switch (user.role) { case 'user': navigate('/user/dashboard'); @@ -55,226 +101,248 @@ const Navbar = () => { } }; - const handleNavClick = (href) => { - if (href.startsWith('#')) { - const element = document.querySelector(href); - if (element) { - element.scrollIntoView({ behavior: 'smooth' }); - } - } - setIsMobileMenuOpen(false); - }; - return ( <> - -
-
- {/* Logo */} - -
- Polijecare Logo -
- - Polijecare - - - - {/* Desktop Navigation */} -
- - {navLinks.map((link, index) => ( - - {link.href.startsWith('#') ? ( - - ) : ( - - {link.name} - - - )} - - ))} - - - {/* Auth Buttons & Theme Toggle */} -
- {isAuthenticated ? ( - <> - - - - ) : ( - - )} - - {/* Theme Toggle */} - -
-
- - {/* Mobile menu button */} -
- -
-
-
- - {/* Mobile menu */} - - {isMobileMenuOpen && ( - -
- {navLinks.map((link, index) => ( - - {link.href.startsWith('#') ? ( - - ) : ( - setIsMobileMenuOpen(false)} - className="block w-full px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium" - > - {link.name} - - )} - - ))} - -
+ + {isScrolled ? ( + + link.href === activeLink)} + onChange={(index) => { + if (index !== null) { + const href = navLinks[index].href; + setActiveLink(href); + handleNavClick(href); + } + }} + trailingElement={ +
+ {isAuthenticated ? ( <> ) : ( )}
+ } + /> +
+ ) : ( + +
+
+ {/* Logo Section - Left */} + + Logo Polije + Polijecare Logo + + + {/* Centered Navigation Links */} +
+ {navLinks.map((link) => ( + + ))} +
+ + {/* Right Section - Auth & Theme */} +
+ + + {isAuthenticated ? ( + <> + + + + ) : ( + + )} +
+ + {/* Mobile menu button */} +
+ +
- - )} - - - - {/* Login Modal */} - setIsLoginModalOpen(false)} +
+ + {/* Mobile menu */} + + {isMobileMenuOpen && ( + +
+ {navLinks.map((link, index) => ( + + {link.href.startsWith('#') ? ( + + ) : ( + setIsMobileMenuOpen(false)} + className="block w-full px-4 py-3 text-gray-600 dark:text-gray-300 hover:text-primary dark:hover:text-primary-light hover:bg-gray-50 dark:hover:bg-gray-700 rounded-lg transition-all duration-200 font-medium" + > + {link.name} + + )} + + ))} + +
+ {isAuthenticated ? ( + <> + + + + ) : ( + + )} +
+
+
+ )} +
+
+ )} +
+ + setIsLoginModalOpen(false)} /> ); diff --git a/frontend/src/components/ui/expandable-tabs.jsx b/frontend/src/components/ui/expandable-tabs.jsx new file mode 100644 index 0000000..b8a0b3d --- /dev/null +++ b/frontend/src/components/ui/expandable-tabs.jsx @@ -0,0 +1,115 @@ +"use client";; +import * as React from "react"; +import { AnimatePresence, motion } from "framer-motion"; +import { useOnClickOutside } from "usehooks-ts"; +import { cn } from "@/lib/utils"; + +const buttonVariants = { + initial: { + gap: 0, + paddingLeft: ".5rem", + paddingRight: ".5rem", + }, + animate: (isSelected) => ({ + gap: isSelected ? ".5rem" : 0, + paddingLeft: isSelected ? "2rem" : "1rem", + paddingRight: isSelected ? "2rem" : "1rem", + }), +}; + +const spanVariants = { + initial: { width: 0, opacity: 0 }, + animate: { width: "auto", opacity: 1 }, + exit: { width: 0, opacity: 0 }, +}; + +const transition = { type: "spring", bounce: 0, duration: 0.3 }; + +export function ExpandableTabs({ + tabs, + className, + activeColor = "text-primary", + onChange, + activeTab, + trailingElement +}) { + const [selected, setSelected] = React.useState(null); + const outsideClickRef = React.useRef(null); + + React.useEffect(() => { + if (activeTab !== undefined && activeTab !== null) { + setSelected(activeTab); + } + }, [activeTab]); + + useOnClickOutside(outsideClickRef, () => { + setSelected(null); + onChange?.(null); + }); + + const handleSelect = (index) => { + setSelected(index); + onChange?.(index); + }; + + const Separator = () => ( +