perbaiki navbar
This commit is contained in:
parent
8c5ef3e1cd
commit
b90cdc07d7
|
|
@ -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": {}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": [
|
||||
"./src/*"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 117 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 76 KiB |
|
|
@ -13,32 +13,32 @@ const Hero = ({ heroData }) => {
|
|||
const hero = heroData || defaultHero;
|
||||
|
||||
return (
|
||||
<section
|
||||
id="hero"
|
||||
className="min-h-screen flex items-center bg-gradient-to-br from-gray-50 to-purple-50 dark:from-gray-900 dark:to-purple-900 relative overflow-hidden pt-16 transition-colors duration-300"
|
||||
<section
|
||||
id="hero"
|
||||
className="min-h-screen flex items-center bg-soft-white relative overflow-hidden pt-16 transition-colors duration-300"
|
||||
>
|
||||
{/* Background Decorations */}
|
||||
<div className="absolute inset-0 overflow-hidden">
|
||||
<motion.div
|
||||
className="absolute top-20 left-10 w-72 h-72 bg-accent/10 rounded-full blur-3xl"
|
||||
animate={{
|
||||
<div className="absolute inset-0 overflow-hidden pointer-events-none">
|
||||
<motion.div
|
||||
className="absolute top-[-10%] left-[-5%] w-[500px] h-[500px] bg-primary-light/30 rounded-full blur-[100px]"
|
||||
animate={{
|
||||
scale: [1, 1.2, 1],
|
||||
opacity: [0.3, 0.5, 0.3]
|
||||
opacity: [0.3, 0.5, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 8,
|
||||
transition={{
|
||||
duration: 8,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
/>
|
||||
<motion.div
|
||||
className="absolute bottom-20 right-10 w-96 h-96 bg-primary/10 rounded-full blur-3xl"
|
||||
animate={{
|
||||
<motion.div
|
||||
className="absolute bottom-[-10%] right-[-5%] w-[600px] h-[600px] bg-primary/10 rounded-full blur-[120px]"
|
||||
animate={{
|
||||
scale: [1, 1.3, 1],
|
||||
opacity: [0.3, 0.4, 0.3]
|
||||
opacity: [0.3, 0.4, 0.3]
|
||||
}}
|
||||
transition={{
|
||||
duration: 10,
|
||||
transition={{
|
||||
duration: 10,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut",
|
||||
delay: 2
|
||||
|
|
@ -49,21 +49,21 @@ const Hero = ({ heroData }) => {
|
|||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div className="grid lg:grid-cols-2 gap-12 lg:gap-20 items-center">
|
||||
{/* Left Content */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="space-y-8"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ duration: 0.8 }}
|
||||
>
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="space-y-4"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<motion.h1
|
||||
<motion.h1
|
||||
className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-tight"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
|
|
@ -72,8 +72,8 @@ const Hero = ({ heroData }) => {
|
|||
>
|
||||
{hero.title}
|
||||
</motion.h1>
|
||||
|
||||
<motion.h2
|
||||
|
||||
<motion.h2
|
||||
className="text-xl md:text-2xl lg:text-3xl font-semibold text-primary dark:text-primary-light"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
|
|
@ -84,7 +84,7 @@ const Hero = ({ heroData }) => {
|
|||
</motion.h2>
|
||||
</motion.div>
|
||||
|
||||
<motion.p
|
||||
<motion.p
|
||||
className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed max-w-xl"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
|
|
@ -94,7 +94,7 @@ const Hero = ({ heroData }) => {
|
|||
{hero.description}
|
||||
</motion.p>
|
||||
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="flex flex-col sm:flex-row gap-4 pt-4"
|
||||
variants={slideUp}
|
||||
initial="hidden"
|
||||
|
|
@ -115,7 +115,7 @@ const Hero = ({ heroData }) => {
|
|||
>
|
||||
Butuh Bantuan Darurat
|
||||
</motion.a>
|
||||
|
||||
|
||||
<motion.div
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
|
|
@ -124,7 +124,7 @@ const Hero = ({ heroData }) => {
|
|||
whileHover={{ scale: 1.05 }}
|
||||
whileTap={{ scale: 0.95 }}
|
||||
>
|
||||
<Link
|
||||
<Link
|
||||
to="/artikel"
|
||||
className="px-8 py-4 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold text-center shadow-soft inline-block w-full sm:w-auto"
|
||||
>
|
||||
|
|
@ -134,7 +134,7 @@ const Hero = ({ heroData }) => {
|
|||
</motion.div>
|
||||
|
||||
{/* Trust Indicators */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="flex flex-wrap gap-6 pt-8"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
|
|
@ -145,12 +145,12 @@ const Hero = ({ heroData }) => {
|
|||
<div className="w-2 h-2 bg-accent rounded-full"></div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">100% Rahasia</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-accent rounded-full"></div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">Profesional</span>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="w-2 h-2 bg-accent rounded-full"></div>
|
||||
<span className="text-sm text-gray-600 dark:text-gray-300 font-medium">24/7 Support</span>
|
||||
|
|
@ -159,7 +159,7 @@ const Hero = ({ heroData }) => {
|
|||
</motion.div>
|
||||
|
||||
{/* Right Content - Logo & Branding */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="relative lg:pl-12"
|
||||
variants={slideRight}
|
||||
initial="hidden"
|
||||
|
|
@ -167,66 +167,24 @@ const Hero = ({ heroData }) => {
|
|||
transition={{ duration: 0.8, delay: 0.3 }}
|
||||
>
|
||||
{/* Main Logo Container */}
|
||||
<motion.div
|
||||
className="relative z-10"
|
||||
animate={{
|
||||
y: [0, -20, 0],
|
||||
}}
|
||||
transition={{
|
||||
duration: 6,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
<motion.div
|
||||
className="relative z-10 flex justify-end lg:pr-4"
|
||||
>
|
||||
<div className="bg-gradient-to-br from-primary/10 to-accent/10 dark:from-primary/20 dark:to-accent/20 rounded-3xl p-12 backdrop-blur-sm border border-white/50 dark:border-gray-700/50 shadow-soft">
|
||||
<div className="aspect-square max-w-md mx-auto flex flex-col items-center justify-center space-y-6">
|
||||
{/* Logo Image */}
|
||||
<motion.div
|
||||
animate={{
|
||||
scale: [1, 1.05, 1],
|
||||
}}
|
||||
transition={{
|
||||
duration: 4,
|
||||
repeat: Infinity,
|
||||
ease: "easeInOut"
|
||||
}}
|
||||
className="relative"
|
||||
>
|
||||
<div className="w-48 h-48 bg-white dark:bg-gray-800 rounded-2xl shadow-lg flex items-center justify-center p-6">
|
||||
<img
|
||||
src="/logo_polijecare.png"
|
||||
alt="Polijecare Logo"
|
||||
className="w-full h-full object-contain"
|
||||
/>
|
||||
</div>
|
||||
{/* Glow Effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-primary/20 to-accent/20 rounded-2xl blur-xl -z-10"></div>
|
||||
</motion.div>
|
||||
|
||||
{/* Brand Text */}
|
||||
<motion.div
|
||||
className="text-center space-y-2"
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.5 }}
|
||||
>
|
||||
<h3 className="text-2xl font-bold text-gray-900 dark:text-white">Polijecare</h3>
|
||||
<p className="text-primary dark:text-primary-light font-medium">Satgas PPKPT Polije</p>
|
||||
<div className="w-16 h-1 bg-gradient-to-r from-primary to-accent rounded-full mx-auto"></div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
<img
|
||||
src="/gambar_header.png"
|
||||
alt="header gambar"
|
||||
className="w-full max-w-[500px] h-auto object-cover"
|
||||
/>
|
||||
</motion.div>
|
||||
|
||||
{/* Background Shape */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-100/50 dark:from-purple-900/20 to-accent/10 dark:to-accent/20 rounded-3xl blur-2xl -z-10"></div>
|
||||
{/* Background Shape */}
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-purple-100/50 dark:from-purple-900/20 to-accent/10 dark:to-accent/20 rounded-3xl blur-2xl -z-10"></div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Scroll Indicator */}
|
||||
<motion.div
|
||||
<motion.div
|
||||
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
import React, { useState, useEffect } from 'react';
|
||||
import { Home, Info, FileText, BookOpen, Phone } from 'lucide-react';
|
||||
import { ExpandableTabs } from "@/components/ui/expandable-tabs";
|
||||
import { Link, useNavigate, useLocation } from 'react-router-dom';
|
||||
import { motion, AnimatePresence } from 'framer-motion';
|
||||
import { useAuth } from '../hooks/useAuth';
|
||||
|
|
@ -6,8 +8,10 @@ import { fadeIn, slideDown } from '../utils/motionVariants';
|
|||
import ThemeToggle from './ThemeToggle';
|
||||
import LoginModal from './LoginModal';
|
||||
|
||||
|
||||
const Navbar = () => {
|
||||
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 (
|
||||
<>
|
||||
<motion.nav
|
||||
className={`fixed top-0 left-0 right-0 z-50 transition-all duration-300 ${
|
||||
isScrolled
|
||||
? 'bg-white/95 backdrop-blur-md shadow-soft border-b border-gray-100'
|
||||
: 'bg-white shadow-sm'
|
||||
}`}
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div className="flex items-center justify-between h-16">
|
||||
{/* Logo */}
|
||||
<Link to="/" className="flex items-center space-x-3">
|
||||
<div className="w-10 h-10 bg-gradient-to-br from-primary to-accent rounded-xl flex items-center justify-center shadow-lg">
|
||||
<img
|
||||
src="/logo_polijecare.png"
|
||||
alt="Polijecare Logo"
|
||||
className="w-8 h-8 object-contain"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-xl font-bold text-gray-900 dark:text-white">
|
||||
Polijecare
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Desktop Navigation */}
|
||||
<div className="hidden md:flex items-center space-x-8">
|
||||
<motion.div
|
||||
className="flex space-x-6"
|
||||
variants={slideDown}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
>
|
||||
{navLinks.map((link, index) => (
|
||||
<motion.div
|
||||
key={link.name}
|
||||
variants={fadeIn}
|
||||
transition={{ delay: 0.1 * index }}
|
||||
>
|
||||
{link.href.startsWith('#') ? (
|
||||
<button
|
||||
onClick={() => handleNavClick(link.href)}
|
||||
className="text-gray-600 hover:text-primary font-medium transition-colors relative group"
|
||||
>
|
||||
{link.name}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={link.href}
|
||||
className="text-gray-600 hover:text-primary font-medium transition-colors relative group"
|
||||
>
|
||||
{link.name}
|
||||
<span className="absolute -bottom-1 left-0 w-0 h-0.5 bg-primary transition-all duration-300 group-hover:w-full"></span>
|
||||
</Link>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
</motion.div>
|
||||
|
||||
{/* Auth Buttons & Theme Toggle */}
|
||||
<div className="flex items-center space-x-4">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDashboardRedirect}
|
||||
className="px-4 py-2 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-soft font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-4 py-2 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
|
||||
>
|
||||
Keluar
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsLoginModalOpen(true)}
|
||||
className="px-6 py-2.5 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 hover:shadow-soft font-medium"
|
||||
>
|
||||
Masuk
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Theme Toggle */}
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="text-gray-600 hover:text-primary p-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<AnimatePresence mode="wait">
|
||||
{isMobileMenuOpen ? (
|
||||
<motion.path
|
||||
key="close"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
) : (
|
||||
<motion.path
|
||||
key="menu"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
className="md:hidden absolute top-full left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="px-4 py-6 space-y-4">
|
||||
{navLinks.map((link, index) => (
|
||||
<motion.div
|
||||
key={link.name}
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.05 * index }}
|
||||
>
|
||||
{link.href.startsWith('#') ? (
|
||||
<button
|
||||
onClick={() => handleNavClick(link.href)}
|
||||
className="block w-full text-left 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}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={link.href}
|
||||
onClick={() => 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}
|
||||
</Link>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className="pt-4 border-t border-gray-100 dark:border-gray-700 space-y-3">
|
||||
<AnimatePresence>
|
||||
{isScrolled ? (
|
||||
<motion.div
|
||||
key="expandable-tabs"
|
||||
initial={{ opacity: 0, scale: 0.8, y: -20, x: "-50%" }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0, x: "-50%" }}
|
||||
exit={{ opacity: 0, scale: 0.8, y: -20, x: "-50%", transition: { duration: 0.1 } }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="fixed top-4 left-1/2 z-50 transform -translate-x-1/2"
|
||||
>
|
||||
<ExpandableTabs
|
||||
tabs={tabs}
|
||||
activeTab={navLinks.findIndex(link => link.href === activeLink)}
|
||||
onChange={(index) => {
|
||||
if (index !== null) {
|
||||
const href = navLinks[index].href;
|
||||
setActiveLink(href);
|
||||
handleNavClick(href);
|
||||
}
|
||||
}}
|
||||
trailingElement={
|
||||
<div className="flex items-center gap-2">
|
||||
<ThemeToggle />
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDashboardRedirect}
|
||||
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
|
||||
className="px-4 py-2 text-sm font-medium bg-[#191970] text-white rounded-full hover:bg-blue-900 transition-colors"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-3 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
|
||||
className="px-4 py-2 text-sm font-medium border border-[#191970] text-[#191970] rounded-full hover:bg-gray-100 transition-colors"
|
||||
>
|
||||
Keluar
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLoginModalOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
|
||||
onClick={() => setIsLoginModalOpen(true)}
|
||||
className="px-6 py-2 text-sm font-medium bg-[#191970] text-white rounded-full hover:bg-blue-900 transition-colors shadow-lg"
|
||||
>
|
||||
Masuk
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.nav
|
||||
key="navbar"
|
||||
initial={{ opacity: 0, scale: 0.9, y: -20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.9, y: -20, transition: { duration: 0.1 } }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
className="fixed top-0 left-0 right-0 z-50 transition-all duration-300 bg-white/50 backdrop-blur-md border-b border-white/20 shadow-sm"
|
||||
>
|
||||
<div className="w-full px-4">
|
||||
<div className="flex items-center justify-between h-20">
|
||||
{/* Logo Section - Left */}
|
||||
<Link to="/" className="flex items-center space-x-2 cursor-default">
|
||||
<img
|
||||
src="/logo_polije.png"
|
||||
alt="Logo Polije"
|
||||
className="h-12 w-auto object-contain"
|
||||
/>
|
||||
<img
|
||||
src="/logo_polijecare.png"
|
||||
alt="Polijecare Logo"
|
||||
className="h-12 w-auto object-contain"
|
||||
/>
|
||||
</Link>
|
||||
|
||||
{/* Centered Navigation Links */}
|
||||
<div className="hidden md:flex items-center space-x-1">
|
||||
{navLinks.map((link) => (
|
||||
<button
|
||||
key={link.name}
|
||||
onClick={() => {
|
||||
setActiveLink(link.href);
|
||||
handleNavClick(link.href);
|
||||
}}
|
||||
className={`px-5 py-2.5 text-base font-medium rounded-full transition-all duration-200 ${activeLink === link.href
|
||||
? 'bg-[#191970] text-white shadow-[0_4px_15px_rgba(25,25,112,0.4)]'
|
||||
: 'text-gray-600 hover:text-[#191970] hover:bg-gray-100'
|
||||
}`}
|
||||
>
|
||||
{link.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Right Section - Auth & Theme */}
|
||||
<div className="hidden md:flex items-center space-x-4">
|
||||
<ThemeToggle />
|
||||
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDashboardRedirect}
|
||||
className="px-5 py-2.5 bg-primary text-white rounded-full hover:bg-primary-dark transition-all duration-300 hover:shadow-lg font-medium text-sm"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="px-5 py-2.5 border-2 border-primary text-primary rounded-full hover:bg-primary hover:text-white transition-all duration-300 font-medium text-sm"
|
||||
>
|
||||
Keluar
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => setIsLoginModalOpen(true)}
|
||||
className="px-6 py-2.5 bg-primary text-white rounded-full hover:bg-primary-dark transition-all duration-300 hover:shadow-lg font-medium text-sm"
|
||||
>
|
||||
Masuk
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu button */}
|
||||
<div className="md:hidden">
|
||||
<button
|
||||
onClick={() => setIsMobileMenuOpen(!isMobileMenuOpen)}
|
||||
className="text-gray-600 hover:text-primary p-2 rounded-lg hover:bg-gray-50 transition-colors"
|
||||
>
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<AnimatePresence mode="wait">
|
||||
{isMobileMenuOpen ? (
|
||||
<motion.path
|
||||
key="close"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M6 18L18 6M6 6l12 12"
|
||||
initial={{ opacity: 0, rotate: -90 }}
|
||||
animate={{ opacity: 1, rotate: 0 }}
|
||||
exit={{ opacity: 0, rotate: 90 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
) : (
|
||||
<motion.path
|
||||
key="menu"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M4 6h16M4 12h16M4 18h16"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
/>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.nav>
|
||||
|
||||
{/* Login Modal */}
|
||||
<LoginModal
|
||||
isOpen={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
</div>
|
||||
|
||||
{/* Mobile menu */}
|
||||
<AnimatePresence>
|
||||
{isMobileMenuOpen && (
|
||||
<motion.div
|
||||
className="md:hidden absolute top-full left-0 right-0 bg-white dark:bg-gray-800 border-t border-gray-200 dark:border-gray-700 shadow-lg"
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -10 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
>
|
||||
<div className="px-4 py-6 space-y-4">
|
||||
{navLinks.map((link, index) => (
|
||||
<motion.div
|
||||
key={link.name}
|
||||
variants={fadeIn}
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
transition={{ delay: 0.05 * index }}
|
||||
>
|
||||
{link.href.startsWith('#') ? (
|
||||
<button
|
||||
onClick={() => handleNavClick(link.href)}
|
||||
className="block w-full text-left 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}
|
||||
</button>
|
||||
) : (
|
||||
<Link
|
||||
to={link.href}
|
||||
onClick={() => 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}
|
||||
</Link>
|
||||
)}
|
||||
</motion.div>
|
||||
))}
|
||||
|
||||
<div className="pt-4 border-t border-gray-100 dark:border-gray-700 space-y-3">
|
||||
{isAuthenticated ? (
|
||||
<>
|
||||
<button
|
||||
onClick={handleDashboardRedirect}
|
||||
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
|
||||
>
|
||||
Dashboard
|
||||
</button>
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="w-full px-4 py-3 border-2 border-primary text-primary rounded-xl hover:bg-primary hover:text-white transition-all duration-300 font-medium"
|
||||
>
|
||||
Keluar
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setIsLoginModalOpen(true);
|
||||
setIsMobileMenuOpen(false);
|
||||
}}
|
||||
className="w-full px-4 py-3 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
|
||||
>
|
||||
Masuk
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.nav>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
<LoginModal
|
||||
isOpen={isLoginModalOpen}
|
||||
onClose={() => setIsLoginModalOpen(false)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 = () => (
|
||||
<div className="mx-1 h-[24px] w-[1.2px] bg-border" aria-hidden="true" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={outsideClickRef}
|
||||
className={cn(
|
||||
"flex flex-wrap items-center gap-2 rounded-full border border-white/20 bg-white/80 backdrop-blur-md p-1 shadow-lg",
|
||||
className
|
||||
)}>
|
||||
{tabs.map((tab, index) => {
|
||||
if (tab.type === "separator") {
|
||||
return <Separator key={`separator-${index}`} />;
|
||||
}
|
||||
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<motion.button
|
||||
key={tab.title}
|
||||
layout
|
||||
variants={buttonVariants}
|
||||
initial={false}
|
||||
animate="animate"
|
||||
custom={selected === index}
|
||||
onClick={() => handleSelect(index)}
|
||||
transition={transition}
|
||||
className={cn(
|
||||
"relative flex items-center rounded-full py-3 text-base font-medium transition-colors duration-300",
|
||||
selected === index
|
||||
? "bg-[#191970] text-white shadow-[0_4px_15px_rgba(25,25,112,0.4)]"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground"
|
||||
)}>
|
||||
<Icon size={24} />
|
||||
<AnimatePresence initial={false}>
|
||||
{selected === index && (
|
||||
<motion.span
|
||||
variants={spanVariants}
|
||||
initial="initial"
|
||||
animate="animate"
|
||||
exit="exit"
|
||||
transition={transition}
|
||||
className="overflow-hidden whitespace-nowrap">
|
||||
{tab.title}
|
||||
</motion.span>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.button>
|
||||
);
|
||||
})}
|
||||
{trailingElement && (
|
||||
<>
|
||||
<Separator />
|
||||
<div className="pl-1">
|
||||
{trailingElement}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
/* Tailwind CSS Directives - v3.4.1 */
|
||||
/* Tailwind CSS Directives - v3.4.1 */
|
||||
/* eslint-disable-next-line at-rule-no-unknown */
|
||||
@tailwind base;
|
||||
/* eslint-disable-next-line at-rule-no-unknown */
|
||||
|
|
@ -8,16 +8,25 @@
|
|||
|
||||
/* Custom CSS Variables and Overrides */
|
||||
:root {
|
||||
--color-primary: #E6E6FA; /* Lavender */
|
||||
--color-primary-dark: #D8BFD8; /* Thistle */
|
||||
--color-accent: #4C6EF5; /* Professional Blue */
|
||||
--color-primary: #e6e6fa;
|
||||
/* Refined Lavender/Purple for better visibility */
|
||||
--color-primary-light: #E9D5FF;
|
||||
/* Soft Lavender for backgrounds */
|
||||
--color-primary-dark: #805AD5;
|
||||
--color-accent: #4C6EF5;
|
||||
/* Professional Blue */
|
||||
--color-accent-light: #5C7CFA;
|
||||
--color-secondary: #495057; /* Professional Gray */
|
||||
--color-secondary: #495057;
|
||||
/* Professional Gray */
|
||||
--color-secondary-light: #6C757D;
|
||||
--color-danger: #dc2626;
|
||||
--color-danger-light: #ef4444;
|
||||
--color-white: #ffffff;
|
||||
--color-gray-50: #f9fafb;
|
||||
--color-soft-white: #F8F9FA;
|
||||
/* Soft White Foundation */
|
||||
--color-gray-50: #F8F9FA;
|
||||
/* Matches soft white */
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-100: #f3f4f6;
|
||||
--color-gray-200: #e5e7eb;
|
||||
--color-gray-300: #d1d5db;
|
||||
|
|
@ -31,13 +40,16 @@
|
|||
|
||||
/* Dark Mode Variables */
|
||||
.dark {
|
||||
--color-primary: #D8BFD8; /* Darker Lavender */
|
||||
--color-primary: #D8BFD8;
|
||||
/* Darker Lavender */
|
||||
--color-primary-dark: #C8B2DB;
|
||||
--color-accent: #5C7CFA; /* Lighter Blue for dark mode */
|
||||
--color-accent: #5C7CFA;
|
||||
/* Lighter Blue for dark mode */
|
||||
--color-accent-light: #7C8FFA;
|
||||
--color-secondary: #6C757D;
|
||||
--color-secondary-light: #868E96;
|
||||
--color-white: #1f2937; /* Dark background */
|
||||
--color-white: #1f2937;
|
||||
/* Dark background */
|
||||
--color-gray-50: #374151;
|
||||
--color-gray-100: #4b5563;
|
||||
--color-gray-200: #6b7280;
|
||||
|
|
@ -353,18 +365,40 @@ body {
|
|||
background-color: var(--color-gray-50);
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5, h6 {
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
font-weight: 600;
|
||||
line-height: 1.2;
|
||||
color: var(--color-gray-900);
|
||||
}
|
||||
|
||||
h1 { font-size: 2.5rem; }
|
||||
h2 { font-size: 2rem; }
|
||||
h3 { font-size: 1.5rem; }
|
||||
h4 { font-size: 1.25rem; }
|
||||
h5 { font-size: 1.125rem; }
|
||||
h6 { font-size: 1rem; }
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
h6 {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
p {
|
||||
margin-bottom: 1rem;
|
||||
|
|
@ -380,6 +414,61 @@ body {
|
|||
a:hover {
|
||||
color: var(--color-primary-dark);
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 0 0% 3.9%;
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 0 0% 3.9%;
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 0 0% 3.9%;
|
||||
--primary: 0 0% 9%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
--secondary: 0 0% 96.1%;
|
||||
--secondary-foreground: 0 0% 9%;
|
||||
--muted: 0 0% 96.1%;
|
||||
--muted-foreground: 0 0% 45.1%;
|
||||
--accent: 0 0% 96.1%;
|
||||
--accent-foreground: 0 0% 9%;
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 89.8%;
|
||||
--input: 0 0% 89.8%;
|
||||
--ring: 0 0% 3.9%;
|
||||
--chart-1: 12 76% 61%;
|
||||
--chart-2: 173 58% 39%;
|
||||
--chart-3: 197 37% 24%;
|
||||
--chart-4: 43 74% 66%;
|
||||
--chart-5: 27 87% 67%;
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 0 0% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 9%;
|
||||
--secondary: 0 0% 14.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 14.9%;
|
||||
--muted-foreground: 0 0% 63.9%;
|
||||
--accent: 0 0% 14.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 14.9%;
|
||||
--input: 0 0% 14.9%;
|
||||
--ring: 0 0% 83.1%;
|
||||
--chart-1: 220 70% 50%;
|
||||
--chart-2: 160 60% 45%;
|
||||
--chart-3: 30 80% 55%;
|
||||
--chart-4: 280 65% 60%;
|
||||
--chart-5: 340 75% 55%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Tailwind CSS Components Layer */
|
||||
|
|
@ -451,20 +540,39 @@ body {
|
|||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
h1 { font-size: 2rem; }
|
||||
h2 { font-size: 1.75rem; }
|
||||
h3 { font-size: 1.25rem; }
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
|
||||
/* tailwindcss@3.4.1 - Mobile Responsive Overrides */
|
||||
.container {
|
||||
@apply px-3;
|
||||
}
|
||||
|
||||
|
||||
.btn {
|
||||
@apply px-5 py-2.5 text-sm;
|
||||
}
|
||||
|
||||
|
||||
.card {
|
||||
@apply p-6;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
border-color: hsl(var(--border));
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { clsx } from "clsx";
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs) {
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
|
@ -4,77 +4,119 @@ export default {
|
|||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
darkMode: 'class',
|
||||
darkMode: ['class', "class"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: '#E6E6FA',
|
||||
dark: '#D8BFD8',
|
||||
light: '#F0E6FF',
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: '#4C6EF5',
|
||||
light: '#5C7CFA',
|
||||
dark: '#364FC7',
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: '#495057',
|
||||
light: '#6C757D',
|
||||
dark: '#343A40',
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DC2626',
|
||||
light: '#EF4444',
|
||||
dark: '#B91C1C',
|
||||
},
|
||||
gray: {
|
||||
50: '#F5F7FA',
|
||||
100: '#E5E7EB',
|
||||
200: '#D1D5DB',
|
||||
300: '#9CA3AF',
|
||||
400: '#6B7280',
|
||||
500: '#4B5563',
|
||||
600: '#374151',
|
||||
700: '#1F2937',
|
||||
800: '#111827',
|
||||
900: '#030712',
|
||||
},
|
||||
purple: {
|
||||
50: '#F3E8FF',
|
||||
100: '#E9D5FF',
|
||||
200: '#D8B4FE',
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'system-ui', 'sans-serif'],
|
||||
},
|
||||
spacing: {
|
||||
'18': '4.5rem',
|
||||
'80': '20rem',
|
||||
'88': '22rem',
|
||||
},
|
||||
borderRadius: {
|
||||
'xl': '0.75rem',
|
||||
'2xl': '1rem',
|
||||
'3xl': '1.5rem',
|
||||
},
|
||||
boxShadow: {
|
||||
'soft': '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
|
||||
'card': '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
'card-hover': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
animation: {
|
||||
'float': 'float 3s ease-in-out infinite',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite',
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0px)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
},
|
||||
},
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
DEFAULT: 'hsl(var(--primary))',
|
||||
dark: '#D8BFD8',
|
||||
light: '#F0E6FF',
|
||||
foreground: 'hsl(var(--primary-foreground))'
|
||||
},
|
||||
accent: {
|
||||
DEFAULT: 'hsl(var(--accent))',
|
||||
light: '#5C7CFA',
|
||||
dark: '#364FC7',
|
||||
foreground: 'hsl(var(--accent-foreground))'
|
||||
},
|
||||
secondary: {
|
||||
DEFAULT: 'hsl(var(--secondary))',
|
||||
light: '#6C757D',
|
||||
dark: '#343A40',
|
||||
foreground: 'hsl(var(--secondary-foreground))'
|
||||
},
|
||||
danger: {
|
||||
DEFAULT: '#DC2626',
|
||||
light: '#EF4444',
|
||||
dark: '#B91C1C'
|
||||
},
|
||||
gray: {
|
||||
'50': '#F5F7FA',
|
||||
'100': '#E5E7EB',
|
||||
'200': '#D1D5DB',
|
||||
'300': '#9CA3AF',
|
||||
'400': '#6B7280',
|
||||
'500': '#4B5563',
|
||||
'600': '#374151',
|
||||
'700': '#1F2937',
|
||||
'800': '#111827',
|
||||
'900': '#030712'
|
||||
},
|
||||
purple: {
|
||||
'50': '#F3E8FF',
|
||||
'100': '#E9D5FF',
|
||||
'200': '#D8B4FE'
|
||||
},
|
||||
background: 'hsl(var(--background))',
|
||||
foreground: 'hsl(var(--foreground))',
|
||||
card: {
|
||||
DEFAULT: 'hsl(var(--card))',
|
||||
foreground: 'hsl(var(--card-foreground))'
|
||||
},
|
||||
popover: {
|
||||
DEFAULT: 'hsl(var(--popover))',
|
||||
foreground: 'hsl(var(--popover-foreground))'
|
||||
},
|
||||
muted: {
|
||||
DEFAULT: 'hsl(var(--muted))',
|
||||
foreground: 'hsl(var(--muted-foreground))'
|
||||
},
|
||||
destructive: {
|
||||
DEFAULT: 'hsl(var(--destructive))',
|
||||
foreground: 'hsl(var(--destructive-foreground))'
|
||||
},
|
||||
border: 'hsl(var(--border))',
|
||||
input: 'hsl(var(--input))',
|
||||
ring: 'hsl(var(--ring))',
|
||||
chart: {
|
||||
'1': 'hsl(var(--chart-1))',
|
||||
'2': 'hsl(var(--chart-2))',
|
||||
'3': 'hsl(var(--chart-3))',
|
||||
'4': 'hsl(var(--chart-4))',
|
||||
'5': 'hsl(var(--chart-5))'
|
||||
}
|
||||
},
|
||||
fontFamily: {
|
||||
sans: [
|
||||
'Inter',
|
||||
'system-ui',
|
||||
'sans-serif'
|
||||
]
|
||||
},
|
||||
spacing: {
|
||||
'18': '4.5rem',
|
||||
'80': '20rem',
|
||||
'88': '22rem'
|
||||
},
|
||||
borderRadius: {
|
||||
xl: '0.75rem',
|
||||
'2xl': '1rem',
|
||||
'3xl': '1.5rem',
|
||||
lg: 'var(--radius)',
|
||||
md: 'calc(var(--radius) - 2px)',
|
||||
sm: 'calc(var(--radius) - 4px)'
|
||||
},
|
||||
boxShadow: {
|
||||
soft: '0 2px 15px -3px rgba(0, 0, 0, 0.07), 0 10px 20px -2px rgba(0, 0, 0, 0.04)',
|
||||
card: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
|
||||
'card-hover': '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)'
|
||||
},
|
||||
animation: {
|
||||
float: 'float 3s ease-in-out infinite',
|
||||
'pulse-slow': 'pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite'
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': {
|
||||
transform: 'translateY(0px)'
|
||||
},
|
||||
'50%': {
|
||||
transform: 'translateY(-10px)'
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [],
|
||||
plugins: [require("tailwindcss-animate")],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import path from "path"
|
||||
import { fileURLToPath } from "url"
|
||||
import react from "@vitejs/plugin-react"
|
||||
import { defineConfig } from "vite"
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url)
|
||||
const __dirname = path.dirname(__filename)
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
resolve: {
|
||||
alias: {
|
||||
"@": path.resolve(__dirname, "./src"),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue