Merge pull request #1 from Annisanzn/nisa2

Nisa2
This commit is contained in:
Annisa Nurul Hidayatil Jannah 2026-02-17 17:20:53 +07:00 committed by GitHub
commit 5cb3bdafcd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
39 changed files with 4174 additions and 1089 deletions

25
frontend/components.json Normal file
View File

@ -0,0 +1,25 @@
{
"$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": {
"@react-bits": "https://reactbits.dev/r/{name}.json"
}
}

10
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": [
"./src/*"
]
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -10,13 +10,27 @@
"preview": "vite preview"
},
"dependencies": {
"@iconify/react": "^6.0.2",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-switch": "^1.2.6",
"axios": "^1.13.5",
"framer-motion": "^10.0.1",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"embla-carousel-react": "^8.6.0",
"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"
"styled-components": "^6.3.9",
"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",

BIN
frontend/public/Gambar1.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 576 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

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

View File

@ -4,6 +4,8 @@ import { ThemeProvider } from './contexts/ThemeContext';
import { AuthProvider, useAuth } from './contexts/AuthContext';
import ProtectedRoute from './components/ProtectedRoute';
import { ScrollParticles } from './components/ui/scroll-particles';
// Pages
import LandingPage from './pages/LandingPage';
import UserDashboard from './pages/UserDashboard';
@ -13,6 +15,7 @@ import OperatorDashboard from './pages/OperatorDashboard';
function App() {
return (
<ThemeProvider>
<ScrollParticles />
<AuthProvider>
<Router>
<Routes>
@ -22,36 +25,36 @@ function App() {
<Route path="/services" element={<LandingPage />} />
<Route path="/articles" element={<LandingPage />} />
<Route path="/contact" element={<LandingPage />} />
{/* Protected Dashboard Routes */}
<Route
path="/user/dashboard"
<Route
path="/user/dashboard"
element={
<ProtectedRoute requiredRole="user">
<UserDashboard />
</ProtectedRoute>
}
}
/>
<Route
path="/konselor/dashboard"
<Route
path="/konselor/dashboard"
element={
<ProtectedRoute requiredRole="konselor">
<KonselorDashboard />
</ProtectedRoute>
}
}
/>
<Route
path="/operator/dashboard"
<Route
path="/operator/dashboard"
element={
<ProtectedRoute requiredRole="operator">
<OperatorDashboard />
</ProtectedRoute>
}
}
/>
{/* Redirect */}
<Route path="/redirect" element={<RedirectDashboard />} />
{/* 404 */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
@ -64,11 +67,11 @@ function App() {
// Redirect component for role-based navigation
const RedirectDashboard = () => {
const { user, isAuthenticated } = useAuth();
if (!isAuthenticated || !user) {
return <Navigate to="/" replace />;
}
switch (user.role) {
case 'user':
return <Navigate to="/user/dashboard" replace />;

View File

@ -1,239 +1,5 @@
import React from 'react';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, slideLeft, staggerChildren } from '../utils/motionVariants';
import AboutSection from "@/components/ui/about-section";
const About = () => {
const features = [
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 1L3 5v6c0 5.55 3.84 10.74 9 12 5.16-1.26 9-6.45 9-12V5l-9-4z"/>
</svg>
),
title: 'Perlindungan',
description: 'Melindungi korban dan saksi dari segala bentuk ancaman atau intimidasi'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/>
</svg>
),
title: 'Pendampingan',
description: 'Memberikan dukungan psikologis dan hukum yang dibutuhkan'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zm-6 9c-1.1 0-2-.9-2-2s.9-2 2-2 2 .9 2 2-.9 2-2 2zm3.1-9H8.9V6c0-1.71 1.39-3.1 3.1-3.1 1.71 0 3.1 1.39 3.1 3.1v2z"/>
</svg>
),
title: 'Kerahasiaan',
description: 'Menjaga identitas dan informasi pelapor dengan ketat'
},
{
icon: (
<svg className="w-8 h-8" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
),
title: 'Keadilan',
description: 'Memastikan proses yang adil dan transparan untuk semua pihak'
}
];
return (
<section id="about" className="py-20 bg-gradient-to-br from-purple-50 to-purple-100 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 right-10 w-64 h-64 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-10 left-10 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Tentang <span className="text-primary">Satgas PPKPT</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Satuan Tugas Pencegahan dan Penanganan Kekerasan Seksual Politeknik Negeri Jember
</motion.p>
</motion.div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-16 items-center">
{/* Left Content - Logo and Description */}
<motion.div
className="space-y-8"
variants={slideLeft}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{/* PolijeCare Logo */}
<div className="flex justify-center lg:justify-start">
<motion.div
className="relative group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
whileHover={{ scale: 1.05 }}
transition={{ duration: 0.3 }}
>
<div className="relative w-56 h-56">
{/* Outer gradient ring */}
<div className="absolute inset-0 bg-gradient-to-br from-primary to-accent rounded-3xl flex items-center justify-center shadow-soft group-hover:shadow-card transition-all duration-300">
<div className="w-48 h-48 bg-white rounded-2xl flex items-center justify-center">
<div className="w-40 h-40 bg-gradient-to-br from-primary/10 to-accent/10 rounded-xl flex items-center justify-center">
<span className="text-2xl font-bold text-primary">PolijeCare</span>
</div>
</div>
</div>
{/* Floating decoration */}
<motion.div
className="absolute -top-3 -right-3 w-10 h-10 bg-accent rounded-2xl shadow-soft flex items-center justify-center"
animate={{
scale: [1, 1.2, 1],
rotate: [0, 10, -10, 0]
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "easeInOut"
}}
>
<svg className="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/>
</svg>
</motion.div>
</div>
</motion.div>
</div>
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.3 }}
>
<h3 className="text-2xl font-bold text-gray-900">
Layanan Pengaduan dan Pendampingan Terpercaya
</h3>
<p className="text-lg text-gray-600 leading-relaxed">
PolijeCare merupakan kanal resmi pengaduan Satgas PPKPT Politeknik Negeri Jember yang menangani laporan kekerasan seksual secara empati, profesional, dan menjaga kerahasiaan.
</p>
<p className="text-lg text-gray-600 leading-relaxed">
Kami berkomitmen untuk menciptakan lingkungan kampus yang <span className="text-primary font-semibold">aman</span>, <span className="text-primary font-semibold">mendukung</span>, dan <span className="text-primary font-semibold">bebas dari kekerasan seksual</span> bagi seluruh sivitas akademika.
</p>
</motion.div>
{/* Stats */}
<motion.div
className="grid grid-cols-2 gap-6 pt-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<div className="text-center p-4 bg-white rounded-2xl shadow-soft">
<div className="text-3xl font-bold text-primary mb-2">24/7</div>
<div className="text-sm text-gray-600 font-medium">Layanan Darurat</div>
</div>
<div className="text-center p-4 bg-white rounded-2xl shadow-soft">
<div className="text-3xl font-bold text-accent mb-2">100%</div>
<div className="text-sm text-gray-600 font-medium">Rahasia Terjamin</div>
</div>
</motion.div>
</motion.div>
{/* Right Content - Features Grid */}
<motion.div
className="space-y-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
<motion.div
className="grid grid-cols-1 sm:grid-cols-2 gap-6"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{features.map((feature, index) => (
<motion.div
key={feature.title}
className="bg-white rounded-2xl p-6 shadow-soft hover:shadow-card transition-all duration-300 hover:-translate-y-1 group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.02 }}
>
<div className="flex items-start space-x-4">
<div className="flex-shrink-0 w-12 h-12 bg-gradient-to-br from-primary/10 to-accent/10 rounded-xl flex items-center justify-center text-primary group-hover:from-primary/20 group-hover:to-accent/20 transition-all duration-300">
{feature.icon}
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-gray-900 mb-2 group-hover:text-primary transition-colors">
{feature.title}
</h4>
<p className="text-gray-600 text-sm leading-relaxed">
{feature.description}
</p>
</div>
</div>
</motion.div>
))}
</motion.div>
{/* Quote */}
<motion.blockquote
className="bg-gradient-to-r from-primary/10 to-accent/10 rounded-2xl p-6 border-l-4 border-primary"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.6 }}
>
<p className="text-lg text-gray-700 italic font-medium leading-relaxed">
"Setiap individu berhak mendapatkan perlindungan dan rasa aman dalam menempuh pendidikan. Mari bersama-sama menjaga kampus kita sebagai tempat yang aman dan mendukung bagi semua."
</p>
<footer className="mt-4 text-sm text-gray-600 font-semibold">
Satgas PPKPT Polije
</footer>
</motion.blockquote>
</motion.div>
</div>
</div>
</section>
);
};
export default About;
export default function About() {
return <AboutSection />;
}

View File

@ -1,7 +1,8 @@
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { Link } from 'react-router-dom';
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
import { fadeIn, slideUp } from '../utils/motionVariants';
import { Gallery4 } from './gallery4';
const Articles = () => {
const [articles, setArticles] = useState([]);
@ -13,240 +14,126 @@ const Articles = () => {
const mockArticles = [
{
id: 1,
title: 'Pentingnya Menjaga Lingkungan Kampus Aman dari Kekerasan Seksual',
slug: 'pentingnya-menjaga-lingkungan-kampus-aman-dari-kekerasan-seksual',
image: 'articles/safe-campus.jpg',
content: 'Lingkungan kampus yang aman adalah hak setiap sivitas akademika.',
title: 'Guru Besar UGM Diduga Lakukan Kekerasan Seksual, Diberhentikan Sementara',
slug: 'https://www.detik.com/jateng/berita/d-6204001/dugaan-kekerasan-seksual-guru-besar-ugm-dipecat-sebagai-dosen',
image: 'https://images.unsplash.com/photo-1592280771800-45cb10bd3dcf?q=80&w=1740&auto=format&fit=crop',
content: 'Universitas Gadjah Mada (UGM) mengambil tindakan tegas dengan memberhentikan sementara seorang guru besar yang diduga terlibat kasus kekerasan seksual.',
is_published: true,
published_at: '2024-01-07T00:00:00.000000Z'
published_at: '2025-01-20T00:00:00.000000Z'
},
{
id: 2,
title: 'Prosedur Pelaporan Kasus Kekerasan Seksual di Polije',
slug: 'prosedur-pelaporan-kasus-kekerasan-seksual-di-polije',
image: 'articles/reporting-procedure.jpg',
content: 'Prosedur pelaporan kasus kekerasan seksual di Politeknik Negeri Jember.',
title: 'Rektor Universitas Pancasila Nonaktif Jalani Pemeriksaan Kasus Pelecehan',
slug: 'https://metro.tempo.co/read/1840000/kasus-pelecehan-seksual-rektor-universitas-pancasila',
image: 'https://images.unsplash.com/photo-1541339907198-e08756dedf3f?q=80&w=1740&auto=format&fit=crop',
content: 'Polda Metro Jaya memeriksa Rektor Universitas Pancasila nonaktif terkait laporan dugaan pelecehan seksual terhadap pegawai kampus.',
is_published: true,
published_at: '2024-01-05T00:00:00.000000Z'
published_at: '2024-06-15T00:00:00.000000Z'
},
{
id: 3,
title: 'Hak dan Kewajiban Korban dan Pelapor Kekerasan Seksual',
slug: 'hak-dan-kewajiban-korban-dan-pelapor-kekerasan-seksual',
image: 'articles/rights-responsibilities.jpg',
content: 'Sebagai korban atau pelapor kekerasan seksual, Anda memiliki hak-hak.',
title: 'Unand Resmikan Satgas PPK, Perluas Cakupan Penanganan Kekerasan',
slug: 'https://www.unand.ac.id/id/berita-peristiwa/berita/item/5799-resmikan-satgas-ppk-rektor-unand-kawal-kampus-aman.html',
image: 'https://images.unsplash.com/photo-1523050854058-8df90110c9f1?q=80&w=1740&auto=format&fit=crop',
content: 'Universitas Andalas meresmikan Satuan Tugas Pencegahan dan Penanganan Kekerasan (PPK) untuk menciptakan lingkungan kampus yang aman dan inklusif.',
is_published: true,
published_at: '2024-01-03T00:00:00.000000Z'
published_at: '2024-11-10T00:00:00.000000Z'
},
{
id: 4,
title: 'Mahasiswa Unsri Tuntut Penuntasan Kasus Pelecehan Seksual',
slug: 'https://www.cnnindonesia.com/nasional/20211203145209-12-729000/mahasiswa-unsri-demo-tuntut-usut-tuntas-dugaan-pelecehan-seksual',
image: 'https://images.unsplash.com/photo-1555848960-8c3af5e4860c?q=80&w=1740&auto=format&fit=crop',
content: 'Ratusan mahasiswa Universitas Sriwijaya menggelar aksi damai menuntut pengusutan tuntas kasus dugaan pelecehan seksual oleh oknum dosen.',
is_published: true,
published_at: '2024-10-05T00:00:00.000000Z'
},
{
id: 5,
title: 'Kemendikbudristek Cabut Izin Kampus yang Abaikan Kasus Kekerasan Seksual',
slug: 'https://nasional.kompas.com/read/2023/06/07/11261391/izin-23-perguruan-tinggi-dicabut-ada-kampus-yang-abaikan-kasus-kekerasan',
image: 'https://images.unsplash.com/photo-1450101499163-c8848c66ca85?q=80&w=1740&auto=format&fit=crop',
content: 'Kemendikbudristek mengambil langkah tegas mencabut izin operasional perguruan tinggi yang terbukti melakukan pelanggaran berat, termasuk pembiaran kekerasan seksual.',
is_published: true,
published_at: '2024-02-01T00:00:00.000000Z'
},
{
id: 6,
title: 'Puan Maharani: Kampus Harus Jadi Ruang Aman Bebas Kekerasan Seksual',
slug: 'https://www.dpr.go.id/berita/detail/id/35000/t/Ketua+DPR+Minta+Kampus+Jadi+Ruang+Aman+dari+Kekerasan+Seksual',
image: 'https://images.unsplash.com/photo-1557804506-669a67965ba0?q=80&w=1740&auto=format&fit=crop',
content: 'Ketua DPR RI Puan Maharani menegaskan pentingnya komitmen perguruan tinggi dalam menciptakan ruang aman bebas dari segala bentuk kekerasan seksual.',
is_published: true,
published_at: '2024-09-15T00:00:00.000000Z'
},
{
id: 7,
title: 'Komnas Perempuan: Kekerasan Seksual di Lingkungan Pendidikan Masih Mengkhawatirkan',
slug: 'https://www.kompas.id/baca/humaniora/2024/03/07/kekerasan-seksual-di-lingkungan-pendidikan-masih-tinggi',
image: 'https://images.unsplash.com/photo-1573164713988-8665fc963095?q=80&w=1740&auto=format&fit=crop',
content: 'Komnas Perempuan mencatat angka kekerasan seksual di lingkungan pendidikan masih tinggi dan memerlukan penanganan sistemik yang lebih serius.',
is_published: true,
published_at: '2024-03-08T00:00:00.000000Z'
},
{
id: 8,
title: 'Pentingnya Pendidikan Seksual di Kampus untuk Cegah Tindakan Asusila',
slug: 'https://edukasi.kompas.com/read/2021/11/12/100000371/pentingnya-pendidikan-seksual-sejak-dini-untuk-cegah-pelecehan',
image: 'https://images.unsplash.com/photo-1544531586-fde5298cdd40?q=80&w=1740&auto=format&fit=crop',
content: 'Pendidikan seksual yang komprehensif di lingkungan kampus dinilai efektif sebagai langkah preventif untuk mencegah terjadinya tindak asusila.',
is_published: true,
published_at: '2024-01-10T00:00:00.000000Z'
}
];
setArticles(mockArticles);
setLoading(false);
}, []);
const formatDate = (dateString) => {
const options = { year: 'numeric', month: 'long', day: 'numeric' };
return new Date(dateString).toLocaleDateString('id-ID', options);
};
const galleryItems = articles.map(article => ({
id: article.id.toString(),
title: article.title,
description: article.content,
href: article.slug,
image: article.image
}));
const SectionTitle = (
<span>
Artikel & <span className="text-primary">Pengumuman</span>
</span>
);
return (
<section id="articles" className="py-20 bg-white relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-10 right-10 w-64 h-64 bg-primary rounded-full blur-3xl"></div>
<div className="absolute bottom-10 left-10 w-96 h-96 bg-accent rounded-full blur-3xl"></div>
<div id="articles" className="bg-gray-50 dark:bg-gray-900 min-h-screen relative overflow-hidden">
{/* Background Decorations */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-primary/5 rounded-full blur-3xl translate-x-1/3 -translate-y-1/3"></div>
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-blue-500/5 rounded-full blur-3xl -translate-x-1/3 translate-y-1/3"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Artikel & <span className="text-primary">Pengumuman</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Dapatkan informasi terbaru seputar layanan, edukasi, dan pengumuman penting dari Satgas PPKPT Polije.
</motion.p>
</motion.div>
{/* Loading State */}
<div className="relative z-10">
{loading && (
<motion.div
className="flex justify-center items-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="flex flex-col items-center space-y-4">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
<p className="text-gray-600 font-medium">Memuat artikel...</p>
</div>
</motion.div>
<div className="flex justify-center items-center py-32">
<div className="w-12 h-12 border-4 border-primary/20 border-t-primary rounded-full animate-spin"></div>
</div>
)}
{/* Error State */}
{error && (
<motion.div
className="text-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="bg-danger/10 border border-danger/20 rounded-2xl p-8 max-w-md mx-auto">
<div className="w-16 h-16 bg-danger/20 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-danger" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Gagal Memuat Artikel</h3>
<p className="text-gray-600 text-sm mb-4">{error}</p>
<button
onClick={fetchArticles}
className="px-6 py-2 bg-primary text-white rounded-xl hover:bg-primary-dark transition-all duration-300 font-medium"
>
Coba Lagi
</button>
</div>
</motion.div>
<div className="text-center py-32 text-red-500 max-w-lg mx-auto px-4">
<p>{error}</p>
</div>
)}
{/* Articles Grid */}
{!loading && !error && (
<>
{articles.length === 0 ? (
<motion.div
className="text-center py-20"
variants={fadeIn}
initial="hidden"
animate="visible"
>
<div className="bg-gray-50 rounded-2xl p-12 max-w-md mx-auto">
<div className="w-16 h-16 bg-gray-200 rounded-full flex items-center justify-center mx-auto mb-4">
<svg className="w-8 h-8 text-gray-400" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">Belum Ada Artikel</h3>
<p className="text-gray-600 text-sm">Belum ada artikel atau pengumuman yang tersedia saat ini.</p>
</div>
</motion.div>
) : (
<motion.div
className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{articles.map((article, index) => (
<motion.article
key={article.id || index}
className="bg-white rounded-2xl shadow-soft hover:shadow-card transition-all duration-300 hover:-translate-y-2 overflow-hidden group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
>
{/* Article Image */}
<div className="relative h-48 overflow-hidden">
{article.image ? (
<img
src={article.image}
alt={article.title}
className="w-full h-full object-cover group-hover:scale-110 transition-transform duration-500"
/>
) : (
<div className="w-full h-full bg-gradient-to-br from-primary/10 to-accent/10 flex items-center justify-center">
<svg className="w-16 h-16 text-primary/30" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm-5 14H7v-2h7v2zm3-4H7v-2h10v2zm0-4H7V7h10v2z"/>
</svg>
</div>
)}
<div className="absolute inset-0 bg-gradient-to-t from-black/50 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
</div>
{/* Article Content */}
<div className="p-6 space-y-4">
{/* Date */}
<div className="flex items-center text-sm text-gray-500">
<svg className="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M19 3h-1V1h-2v2H8V1H6v2H5c-1.11 0-1.99.9-1.99 2L3 19c0 1.1.89 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V8h14v11zM7 10h5v5H7z"/>
</svg>
{formatDate(article.published_at)}
</div>
{/* Title */}
<h3 className="text-xl font-bold text-gray-900 line-clamp-2 group-hover:text-primary transition-colors">
{article.title}
</h3>
{/* Excerpt */}
<p className="text-gray-600 text-sm leading-relaxed line-clamp-3">
{article.excerpt}
</p>
{/* Read More Link */}
<div className="pt-4">
<Link
to={`/artikel/${article.slug}`}
className="inline-flex items-center text-primary font-semibold text-sm hover:text-primary-dark transition-colors group"
>
Baca Selengkapnya
<svg className="w-4 h-4 ml-1 transform group-hover:translate-x-1 transition-transform" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
</Link>
</div>
</div>
</motion.article>
))}
</motion.div>
)}
</>
)}
{/* View All Button */}
{!loading && !error && articles.length > 0 && (
<motion.div
className="text-center mt-12"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<Link
to="/artikel"
className="inline-flex items-center 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 shadow-soft"
>
Lihat Semua Artikel
<svg className="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 24 24">
<path d="M8.59 16.59L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.41z"/>
</svg>
</Link>
</motion.div>
<Gallery4
title={SectionTitle}
description="Dapatkan informasi terbaru seputar layanan, edukasi, dan pengumuman penting dari Satgas PPKPT Polije. Kami berkomitmen untuk transparansi dan edukasi."
items={galleryItems}
/>
)}
</div>
</section>
</div>
);
};

View File

@ -1,9 +1,12 @@
import React from 'react';
import { Link } from 'react-router-dom';
import { Link, useNavigate } from 'react-router-dom';
import { Component as ReportButton } from './button';
import { Icon } from '@iconify/react';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, slideLeft, slideRight } from '../utils/motionVariants';
const Hero = ({ heroData }) => {
const navigate = useNavigate();
const defaultHero = {
title: 'Aman Bicara, Aman Melapor',
subtitle: 'Satgas PPKPT Politeknik Negeri Jember',
@ -13,32 +16,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
@ -46,25 +49,25 @@ const Hero = ({ heroData }) => {
/>
</div>
<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">
<div className="max-w-[1440px] mx-auto px-8 sm:px-12 lg:px-16 relative z-10">
<div className="grid lg:grid-cols-2 gap-8 lg:gap-12 items-center">
{/* Left Content */}
<motion.div
className="space-y-8"
<motion.div
className="space-y-6"
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
className="text-4xl md:text-5xl lg:text-6xl font-bold text-gray-900 dark:text-white leading-tight"
<motion.h1
className="text-5xl md:text-6xl lg:text-7xl font-bold text-gray-900 dark:text-white leading-tight"
variants={slideUp}
initial="hidden"
animate="visible"
@ -72,9 +75,9 @@ const Hero = ({ heroData }) => {
>
{hero.title}
</motion.h1>
<motion.h2
className="text-xl md:text-2xl lg:text-3xl font-semibold text-primary dark:text-primary-light"
<motion.h2
className="text-2xl md:text-3xl lg:text-4xl font-semibold text-[#191970] dark:text-blue-300"
variants={slideUp}
initial="hidden"
animate="visible"
@ -84,28 +87,35 @@ const Hero = ({ heroData }) => {
</motion.h2>
</motion.div>
<motion.p
className="text-lg text-gray-600 dark:text-gray-300 leading-relaxed max-w-xl"
<motion.p
className="text-xl text-gray-600 dark:text-gray-300 leading-relaxed max-w-2xl"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.5 }}
>
{hero.description}
{hero.description ? (
hero.description
) : (
<>
Kami siap mendengar dan membantu Anda dengan{' '}
<span className="highlight-marker" style={{ '--delay': '1' }}>profesionalisme</span> dan{' '}
<span className="highlight-marker" style={{ '--delay': '1.6' }}>kerahasiaan terjamin</span>.{' '}
Setiap laporan akan ditangani dengan{' '}
<span className="highlight-marker" style={{ '--delay': '2.2' }}>empati</span> dan{' '}
<span className="highlight-marker" style={{ '--delay': '2.6' }}>seksama</span>.
</>
)}
</motion.p>
<motion.div
className="flex flex-col sm:flex-row gap-4 pt-4"
<motion.div
className="flex flex-row gap-4 pt-4 items-start"
variants={slideUp}
initial="hidden"
animate="visible"
transition={{ delay: 0.6 }}
>
<motion.a
href="https://wa.me/6281234567890"
target="_blank"
rel="noopener noreferrer"
className="px-8 py-4 bg-danger text-white rounded-xl hover:bg-danger-dark transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold text-center shadow-soft"
<motion.div
variants={fadeIn}
initial="hidden"
animate="visible"
@ -113,9 +123,17 @@ const Hero = ({ heroData }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
Butuh Bantuan Darurat
</motion.a>
<ReportButton
icon={<Icon icon="solar:phone-calling-bold-duotone" />}
title="Butuh Bantuan Darurat"
size="sm"
className="rounded-full bg-red-600 hover:bg-red-700 border-0"
gradientLight={{ from: "from-red-600", via: "via-red-600", to: "to-red-600" }}
gradientDark={{ from: "from-red-600", via: "via-red-600", to: "to-red-600" }}
onClick={() => window.open('https://wa.me/6281234567890', '_blank')}
/>
</motion.div>
<motion.div
variants={fadeIn}
initial="hidden"
@ -124,42 +142,24 @@ const Hero = ({ heroData }) => {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
<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"
>
Buat Laporan
</Link>
<ReportButton
icon={<Icon icon="solar:document-add-bold-duotone" />}
title="Buat Laporan"
size="sm"
className="rounded-full bg-[#191970] hover:bg-blue-900 border-0"
gradientLight={{ from: "from-[#191970]", via: "via-[#191970]", to: "to-[#191970]" }}
gradientDark={{ from: "from-[#191970]", via: "via-[#191970]", to: "to-[#191970]" }}
onClick={() => navigate('/artikel')}
/>
</motion.div>
</motion.div>
{/* Trust Indicators */}
<motion.div
className="flex flex-wrap gap-6 pt-8"
variants={fadeIn}
initial="hidden"
animate="visible"
transition={{ delay: 0.9 }}
>
<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">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>
</div>
</motion.div>
</motion.div>
{/* Right Content - Logo & Branding */}
<motion.div
<motion.div
className="relative lg:pl-12"
variants={slideRight}
initial="hidden"
@ -167,82 +167,26 @@ 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-[550px] 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>
{/* Brand Stats Banner - Glassmorphic Light Design */}
</div>
{/* Scroll Indicator */}
<motion.div
className="absolute bottom-8 left-1/2 transform -translate-x-1/2"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ delay: 1.5, duration: 0.8 }}
>
<motion.div
animate={{ y: [0, 10, 0] }}
transition={{ duration: 2, repeat: Infinity }}
className="flex flex-col items-center space-y-2"
>
<span className="text-sm text-gray-400 dark:text-gray-500 font-medium">Scroll ke bawah</span>
<div className="w-6 h-10 border-2 border-gray-400 dark:border-gray-500 rounded-full flex justify-center">
<div className="w-1 h-3 bg-gray-600 dark:bg-gray-400 rounded-full mt-2"></div>
</div>
</motion.div>
</motion.div>
</section>
);
};

View File

@ -1,13 +1,17 @@
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';
import { fadeIn, slideDown } from '../utils/motionVariants';
import ThemeToggle from './ThemeToggle';
import Switch from './sky-toggle';
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,62 @@ 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(() => {
let ticking = false;
const handleScroll = () => {
setIsScrolled(window.scrollY > 20);
if (!ticking) {
requestAnimationFrame(() => {
// Logic for switching navbar type (Standard vs Expandable)
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);
}
ticking = false;
});
ticking = true;
}
};
window.addEventListener('scroll', handleScroll);
window.addEventListener('scroll', handleScroll, { passive: true });
return () => window.removeEventListener('scroll', handleScroll);
}, []);
}, [navLinks, activeLink]);
const handleLogout = () => {
logout();
@ -38,7 +90,7 @@ const Navbar = () => {
const handleDashboardRedirect = () => {
if (!user) return;
switch (user.role) {
case 'user':
navigate('/user/dashboard');
@ -55,226 +107,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 mode="wait">
{isScrolled ? (
<motion.div
key="expandable-tabs"
initial={{ opacity: 0, y: -10, x: "-50%" }}
animate={{ opacity: 1, y: 0, x: "-50%" }}
exit={{ opacity: 0, y: -10, x: "-50%", transition: { duration: 0.15 } }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="fixed top-4 left-1/2 z-50 transform -translate-x-1/2 will-change-transform"
>
<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">
<Switch />
{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-5 py-2.5 text-sm font-medium bg-primary text-white rounded-full hover:bg-primary-dark transition-all duration-300 hover:shadow-lg"
>
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-5 py-2.5 text-sm font-medium border-2 border-primary text-primary rounded-full hover:bg-primary hover:text-white transition-all duration-300"
>
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.5 text-sm font-medium bg-[#191970] text-white rounded-full hover:bg-blue-900 transition-all duration-300 hover:shadow-lg"
>
Masuk
</button>
)}
</div>
}
/>
</motion.div>
) : (
<motion.nav
key="navbar"
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10, transition: { duration: 0.15 } }}
transition={{ duration: 0.25, ease: [0.25, 0.46, 0.45, 0.94] }}
className="fixed top-0 left-0 right-0 z-50 transition-colors duration-300 bg-white/50 backdrop-blur-md border-b border-white/20 shadow-sm will-change-transform"
>
<div className="w-full px-8 lg:px-12">
<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">
<Switch />
{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-[#191970] text-white rounded-full hover:bg-blue-900 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)}
/>
</>
);

View File

@ -2,99 +2,119 @@ import React from 'react';
import { Link } from 'react-router-dom';
import { motion } from 'framer-motion';
import { fadeIn, slideUp, staggerChildren } from '../utils/motionVariants';
import {
MessageCircle,
FileText,
Shield,
Users,
HeartHandshake,
Phone,
CheckCircle2,
Search,
Gavel,
Smile
} from 'lucide-react';
import { SpotlightCard } from './ui/spotlight-card';
import { FlowButton } from './ui/flow-button';
const Services = () => {
const reportingMethods = [
{
id: 'whatsapp',
title: 'Via WhatsApp',
description: 'Laporkan secara langsung melalui WhatsApp untuk respons cepat dan konsultasi awal dengan tim kami.',
icon: (
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
<path d="M17.472 14.382c-.297-.149-1.758-.867-2.03-.967-.273-.099-.471-.149-.67.149-.197.297-.767.966-.94 1.164-.173.199-.347.223-.644.074-.297-.149-1.255-.462-2.39-1.475-.883-.788-1.48-1.761-1.653-2.059-.173-.297-.018-.458.13-.606.134-.133.297-.347.446-.521.151-.172.2-.296.3-.495.099-.198.05-.372-.025-.521-.075-.148-.669-1.611-.916-2.206-.242-.579-.487-.501-.669-.51l-.57-.01c-.198 0-.52.074-.792.372s-1.04 1.016-1.04 2.479 1.065 2.876 1.213 3.074c.149.198 2.095 3.2 5.076 4.487.709.306 1.263.489 1.694.626.712.226 1.36.194 1.872.118.571-.085 1.758-.719 2.006-1.413.248-.695.248-1.29.173-1.414-.074-.123-.272-.198-.57-.347m-5.421 7.403h-.004a9.87 9.87 0 01-5.031-1.378l-.361-.214-3.741.982.998-3.648-.235-.374a9.86 9.86 0 01-1.51-5.26c.001-5.45 4.436-9.884 9.888-9.884 2.64 0 5.122 1.03 6.988 2.898a9.825 9.825 0 012.893 6.994c-.003 5.45-4.437 9.884-9.885 9.884m8.413-18.297A11.815 11.815 0 0012.05 0C5.495 0 .16 5.335.157 11.892c0 2.096.547 4.142 1.588 5.945L.057 24l6.305-1.654a11.882 11.882 0 005.683 1.448h.005c6.554 0 11.89-5.335 11.893-11.893a11.821 11.821 0 00-3.48-8.413"/>
</svg>
),
features: ['Respons 24/7', 'Konsultasi awal', 'Bimbingan langkah selanjutnya'],
buttonText: 'Butuh Bantuan Darurat',
buttonColor: 'bg-accent hover:bg-accent-dark',
buttonLink: 'https://wa.me/6281234567890'
description: 'Layanan cepat tanggap untuk konsultasi awal dan pelaporan darurat. Terhubung langsung dengan tim satgas kami.',
icon: <MessageCircle className="w-8 h-8" />,
features: ['Respons 24/7', 'Konsultasi Privat', 'Pendampingan Awal'],
buttonText: 'Chat WhatsApp Sekarang',
buttonColor: 'bg-green-600 hover:bg-green-700',
buttonLink: 'https://wa.me/6281234567890',
gradient: 'from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20',
iconColor: 'text-green-600 dark:text-green-400',
spotlightColor: 'rgba(34, 197, 94, 0.2)' // Green glow for WhatsApp
},
{
id: 'form',
title: 'Form Pengaduan Online',
description: 'Isi form pengaduan secara online dengan detail lengkap dan upload bukti pendukung untuk proses yang lebih terstruktur.',
icon: (
<svg className="w-12 h-12" fill="currentColor" viewBox="0 0 24 24">
<path d="M14 2H6c-1.1 0-2 .9-2 2v16c0 1.1.89 2 2 2h12c1.1 0 2-.9 2-2V8l-6-6zm4 18H6V4h7v5h5v11z"/>
<path d="M8 12h8v2H8zm0 4h8v2H8zm0-8h5v2H8z"/>
</svg>
),
features: ['Form terstruktur', 'Upload bukti', 'Tracking status laporan'],
buttonText: 'Laporkan Sekarang',
buttonColor: 'bg-primary hover:bg-primary-dark',
buttonLink: '/artikel'
title: 'Form Pengaduan',
description: 'Saluran resmi untuk pelaporan mendetail. Mendukung lampiran bukti dan kronologi lengkap untuk investigasi.',
icon: <FileText className="w-8 h-8" />,
features: ['Form Terstruktur', 'Upload Bukti Aman', 'Tracking Status'],
buttonText: 'Isi Form Laporan',
buttonColor: 'bg-[#191970] hover:bg-blue-900',
buttonLink: '/artikel',
gradient: 'from-blue-50 to-indigo-50 dark:from-blue-900/20 dark:to-indigo-900/20',
iconColor: 'text-[#191970] dark:text-blue-400',
spotlightColor: 'rgba(139, 92, 246, 0.2)' // Purple glow for Form
}
];
const handlingFlow = [
{
step: '01',
title: 'Pelaporan',
desc: 'Laporan masuk via WA atau Website.',
icon: <FileText className="w-5 h-5" />
},
{
step: '02',
title: 'Verifikasi',
desc: 'Validasi data oleh tim Satgas.',
icon: <CheckCircle2 className="w-5 h-5" />
},
{
step: '03',
title: 'Tindak Lanjut',
desc: 'Investigasi atau mediasi kasus.',
icon: <Search className="w-5 h-5" />
},
{
step: '04',
title: 'Penyelesaian',
desc: 'Pemulihan dan penutupan kasus.',
icon: <Smile className="w-5 h-5" />
}
];
const importantInfo = [
{
icon: '🛡️',
title: 'Aman',
description: 'Identitas Anda akan dirahasiakan sepenuhnya'
icon: <Shield className="w-6 h-6" />,
title: 'Dijamin Aman',
description: 'Identitas pelapor dirahasiakan sepenuhnya sesuai kode etik.'
},
{
icon: '👥',
icon: <Users className="w-6 h-6" />,
title: 'Profesional',
description: 'Ditangani oleh tim yang berpengalaman'
description: 'Ditangani oleh tim ahli yang berpengalaman dan objektif.'
},
{
icon: '💚',
title: 'Support',
description: 'Dapatkan pendampingan penuh dari kami'
icon: <HeartHandshake className="w-6 h-6" />,
title: 'Pendampingan',
description: 'Dukungan psikologis dan hukum selama proses berjalan.'
}
];
return (
<section id="services" className="py-20 bg-gray-50 relative overflow-hidden">
{/* Background Pattern */}
<div className="absolute inset-0 opacity-5">
<div className="absolute top-20 left-20 w-72 h-72 bg-accent rounded-full blur-3xl"></div>
<div className="absolute bottom-20 right-20 w-96 h-96 bg-primary rounded-full blur-3xl"></div>
<section id="services" className="pt-20 pb-24 bg-gray-50 dark:bg-gray-900 relative overflow-hidden transition-colors duration-300">
{/* Background Decorations */}
<div className="absolute top-0 left-0 w-full h-full overflow-hidden pointer-events-none">
<div className="absolute top-[-10%] right-[-5%] w-96 h-96 bg-purple-200/30 dark:bg-purple-900/10 rounded-full blur-3xl"></div>
<div className="absolute bottom-[-10%] left-[-5%] w-96 h-96 bg-blue-200/30 dark:bg-blue-900/10 rounded-full blur-3xl"></div>
</div>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 relative z-10">
{/* Section Header */}
<motion.div
className="text-center mb-20"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<motion.h2
className="text-4xl md:text-5xl font-bold text-gray-900 mb-6"
variants={slideUp}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
Cara <span className="text-primary">Melapor</span>
</motion.h2>
<motion.p
className="text-xl text-gray-600 max-w-3xl mx-auto leading-relaxed"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 }}
>
Pilih metode pelaporan yang paling nyaman untuk Anda. Kami siap membantu dengan profesionalisme dan kerahasiaan terjamin.
</motion.p>
</motion.div>
{/* Reporting Methods Cards */}
<motion.div
className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-20"
{/* Header Section */}
<div className="text-center mb-12">
<h2 className="text-4xl md:text-5xl font-bold text-gray-900 dark:text-white mb-6 tracking-tight">
Cara <span className="text-[#191970] dark:text-blue-400">Melapor</span>
</h2>
<p className="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto leading-relaxed">
Kami menyediakan ruang aman bagi Anda untuk bersuara. Pilih metode yang paling nyaman, kami siap mendampingi setiap langkahnya.
</p>
</div>
{/* Reporting Methods Grid */}
<motion.div
className="grid grid-cols-1 lg:grid-cols-2 gap-8 mb-24"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
@ -103,145 +123,175 @@ const Services = () => {
{reportingMethods.map((method, index) => (
<motion.div
key={method.id}
className="bg-white rounded-3xl p-8 shadow-soft hover:shadow-card transition-all duration-500 hover:-translate-y-2 group"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.2 * index }}
whileHover={{ y: -5 }}
className="h-full"
>
{/* Icon */}
<motion.div
className="w-20 h-20 bg-gradient-to-br from-primary/10 to-accent/10 rounded-2xl flex items-center justify-center text-primary mb-6 group-hover:from-primary/20 group-hover:to-accent/20 transition-all duration-300"
whileHover={{ scale: 1.1, rotate: 5 }}
transition={{ duration: 0.3 }}
<SpotlightCard
className="h-full p-8 rounded-3xl border border-white/20 dark:border-white/10 bg-white/10 dark:bg-black/20 backdrop-blur-sm shadow-xl transition-all duration-300 group hover:bg-white/15 dark:hover:bg-white/5"
spotlightColor={method.spotlightColor}
>
{method.icon}
</motion.div>
{/* Gradient Blob Background */}
<div className={`absolute top-0 right-0 w-64 h-64 bg-gradient-to-br ${method.gradient} rounded-full blur-3xl -z-10 opacity-30 group-hover:opacity-60 transition-opacity duration-500`}></div>
{/* Content */}
<div className="space-y-4">
<h3 className="text-2xl font-bold text-gray-900 group-hover:text-primary transition-colors">
{method.title}
</h3>
<p className="text-gray-600 leading-relaxed">
{method.description}
</p>
{/* Features */}
<div className="space-y-3">
{method.features.map((feature, featureIndex) => (
<div key={featureIndex} className="flex items-center space-x-3">
<div className="w-2 h-2 bg-accent rounded-full flex-shrink-0"></div>
<span className="text-gray-700 text-sm font-medium">{feature}</span>
<div className="flex flex-col h-full justify-between relative z-10">
<div>
<div className={`w-16 h-16 rounded-2xl bg-white dark:bg-gray-700 shadow-sm flex items-center justify-center mb-6 ${method.iconColor}`}>
{method.icon}
</div>
))}
</div>
{/* Button */}
<div className="pt-6">
{method.buttonLink.startsWith('http') ? (
<a
<h3 className="text-2xl font-bold text-gray-900 dark:text-white mb-3">
{method.title}
</h3>
<p className="text-gray-600 dark:text-gray-300 mb-6 leading-relaxed">
{method.description}
</p>
<ul className="space-y-3 mb-8">
{method.features.map((feature, i) => (
<li key={i} className="flex items-center text-gray-700 dark:text-gray-300 text-sm font-medium">
<CheckCircle2 className={`w-4 h-4 mr-2 ${method.iconColor}`} />
{feature}
</li>
))}
</ul>
</div>
<div className="w-full flex justify-center mt-auto pt-6">
<FlowButton
text={method.buttonText}
href={method.buttonLink}
target="_blank"
rel="noopener noreferrer"
className={`inline-flex items-center justify-center px-8 py-4 ${method.buttonColor} text-white rounded-xl transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold w-full sm:w-auto shadow-soft`}
>
{method.buttonText}
</a>
) : (
<Link
to={method.buttonLink}
className={`inline-flex items-center justify-center px-8 py-4 ${method.buttonColor} text-white rounded-xl transition-all duration-300 hover:shadow-lg hover:-translate-y-1 font-semibold w-full sm:w-auto shadow-soft`}
>
{method.buttonText}
</Link>
)}
target={method.buttonLink.startsWith('http') ? "_blank" : undefined}
colorStr={method.id === 'whatsapp' ? '#16a34a' : '#2563eb'}
hoverColorStr={method.id === 'whatsapp' ? '#16a34a' : '#1e40af'}
className="w-full max-w-[280px]"
/>
</div>
</div>
</div>
</SpotlightCard>
</motion.div>
))}
</motion.div>
{/* Important Information */}
<motion.div
className="bg-gradient-to-r from-purple-50 to-purple-100 rounded-3xl p-8 md:p-12"
{/* Support & Flow Section */}
<div className="grid grid-cols-1 xl:grid-cols-3 gap-8">
{/* Trust Indicators (Left) */}
<motion.div
className="xl:col-span-1 bg-white dark:bg-gray-800 rounded-3xl p-8 border border-gray-100 dark:border-gray-700 shadow-md h-full"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
<h3 className="text-xl font-bold text-gray-900 dark:text-white mb-6">Kenapa Kami?</h3>
<div className="space-y-6">
{importantInfo.map((info, index) => (
<div key={index} className="flex items-start space-x-4">
<div className="w-10 h-10 rounded-full bg-blue-50 dark:bg-blue-900/30 flex items-center justify-center text-[#191970] dark:text-blue-300 flex-shrink-0">
{info.icon}
</div>
<div>
<h4 className="font-semibold text-gray-900 dark:text-white text-sm">{info.title}</h4>
<p className="text-gray-500 dark:text-gray-400 text-xs leading-relaxed mt-1">{info.description}</p>
</div>
</div>
))}
</div>
</motion.div>
{/* Process Flow (Right - Timeline) */}
<SpotlightCard
className="xl:col-span-2 relative overflow-hidden rounded-3xl p-8 md:p-10 border border-white/20 dark:border-white/10 bg-white/10 dark:bg-black/20 backdrop-blur-sm shadow-xl transition-all duration-300 group hover:bg-white/15 dark:hover:bg-white/5"
spotlightColor="rgba(25, 25, 112, 0.1)"
>
{/* Background Pattern */}
<div className="absolute top-0 right-0 w-64 h-64 bg-blue-50 dark:bg-blue-900/10 rounded-full blur-3xl -translate-y-1/2 translate-x-1/2"></div>
<div className="absolute bottom-0 left-0 w-48 h-48 bg-purple-50 dark:bg-purple-900/10 rounded-full blur-3xl translate-y-1/2 -translate-x-1/2"></div>
<h3 className="text-2xl font-bold mb-12 relative z-10 text-gray-900 dark:text-white">Alur Penanganan</h3>
<div className="relative z-10 w-full">
{/* Progress Line Background */}
<div className="hidden md:block absolute top-[28px] left-0 w-full h-[2px] bg-gray-200/50 dark:bg-gray-700/50 rounded-full"></div>
{/* Animated Progress Line */}
<motion.div
className="hidden md:block absolute top-[28px] left-0 h-[2px] bg-[#191970] dark:bg-blue-500 origin-left z-0 rounded-full overflow-hidden"
initial={{ scaleX: 0 }}
whileInView={{ scaleX: 1 }}
viewport={{ once: true }}
transition={{ duration: 1.5, ease: "easeInOut", delay: 0.2 }}
>
{/* Continuous Shimmer Animation */}
<motion.div
className="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-transparent via-white/50 to-transparent"
animate={{ x: ['-100%', '100%'] }}
transition={{
repeat: Infinity,
duration: 2,
ease: "linear",
repeatDelay: 0.5
}}
/>
</motion.div>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-8">
{handlingFlow.map((step, index) => (
<motion.div
key={index}
className="relative group/step pt-4 md:pt-0"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: index * 0.2 + 0.3 }}
>
<div className="relative z-10 flex flex-col items-center text-center">
<div className="w-14 h-14 rounded-full bg-white dark:bg-gray-800 border-2 border-gray-100 dark:border-gray-700 flex items-center justify-center mb-4 text-[#191970] dark:text-blue-500 shadow-md group-hover/step:scale-110 group-hover/step:border-[#191970] dark:group-hover/step:border-blue-500 transition-all duration-300 relative bg-opacity-80 backdrop-blur-md">
{step.icon}
</div>
<span className="text-xs font-bold font-mono text-[#191970] dark:text-blue-400 mb-2 opacity-80">{step.step}</span>
<h4 className="font-bold text-gray-900 dark:text-white text-base mb-2">{step.title}</h4>
<p className="text-sm text-gray-700 dark:text-gray-300 leading-relaxed font-medium">{step.desc}</p>
</div>
</motion.div>
))}
</div>
</div>
</SpotlightCard>
</div>
{/* Emergency Banner */}
<motion.div
className="mt-12 bg-red-50 dark:bg-red-900/10 border border-red-100 dark:border-red-900/30 rounded-2xl p-6 flex flex-col md:flex-row items-center justify-between gap-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.4 }}
>
<div className="text-center mb-8">
<h3 className="text-2xl md:text-3xl font-bold text-gray-900 dark:text-white mb-4">
Penting Untuk Diketahui
</h3>
<p className="text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
Kami memastikan setiap proses pelaporan berjalan dengan aman, profesional, dan mendukung.
</p>
</div>
<motion.div
className="grid grid-cols-1 md:grid-cols-3 gap-6"
variants={staggerChildren}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
>
{importantInfo.map((info, index) => (
<motion.div
key={index}
className="text-center p-6 bg-white rounded-2xl shadow-soft hover:shadow-card transition-all duration-300"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.1 * index }}
whileHover={{ scale: 1.05 }}
>
<div className="text-4xl mb-4">{info.icon}</div>
<h4 className="text-lg font-bold text-gray-900 mb-2">{info.title}</h4>
<p className="text-gray-600 text-sm leading-relaxed">{info.description}</p>
</motion.div>
))}
</motion.div>
</motion.div>
{/* Emergency Notice */}
<motion.div
className="mt-12 bg-gradient-to-r from-danger/10 to-danger/5 border border-danger/20 rounded-2xl p-6"
variants={fadeIn}
initial="hidden"
whileInView="visible"
viewport={{ once: true }}
transition={{ delay: 0.6 }}
>
<div className="flex items-center space-x-4">
<div className="flex-shrink-0">
<div className="w-12 h-12 bg-danger/20 rounded-xl flex items-center justify-center">
<svg className="w-6 h-6 text-danger" fill="currentColor" viewBox="0 0 24 24">
<path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-2h2v2zm0-4h-2V7h2v6z"/>
</svg>
</div>
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-red-100 dark:bg-red-900/30 rounded-full flex items-center justify-center text-red-600 dark:text-red-400">
<Phone className="w-6 h-6" />
</div>
<div className="flex-1">
<h4 className="text-lg font-semibold text-gray-900 mb-1">Darurat? Hubungi Kami Sekarang</h4>
<p className="text-gray-600 text-sm">
Jika Anda atau orang lain berada dalam situasi darurat, segera hubungi WhatsApp kami untuk respons cepat 24/7.
</p>
</div>
<div className="flex-shrink-0">
<a
href="https://wa.me/6281234567890"
target="_blank"
rel="noopener noreferrer"
className="px-6 py-3 bg-danger text-white rounded-xl hover:bg-danger-dark transition-all duration-300 font-semibold shadow-soft hover:shadow-lg"
>
Hubungi Darurat
</a>
<div>
<h4 className="font-bold text-gray-900 dark:text-white">Butuh Bantuan Darurat?</h4>
<p className="text-sm text-gray-600 dark:text-gray-300">Jangan ragu untuk menghubungi kami jika situasi mendesak.</p>
</div>
</div>
<FlowButton
text="Hubungi Sekarang"
href="https://wa.me/6281234567890"
target="_blank"
colorStr="#dc2626"
hoverColorStr="#dc2626"
className="w-auto px-8"
/>
</motion.div>
</div>
</section>
);

View File

@ -0,0 +1,53 @@
import React from "react";
export const Component = ({
icon,
title,
subtitle,
size = "md",
className = "",
...props
}) => {
const sizes = {
sm: "px-10 py-4 rounded-full text-base",
md: "p-4 rounded-2xl",
lg: "p-6 rounded-3xl",
};
return (
<button
{...props}
className={`group relative overflow-hidden cursor-pointer transition-all duration-300 ease-out
shadow-lg hover:shadow-xl hover:scale-[1.02] hover:-translate-y-1 active:scale-95
border-0 flex items-center justify-between gap-6
${sizes[size]}
${className}`}>
{/* Subtle internal glow/shine for depth, but keeping base color solid */}
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/10 to-transparent -translate-x-full group-hover:translate-x-full transition-transform duration-1000 ease-out"></div>
{/* Content - flat structure for perfect alignment */}
<div className="relative z-10 flex items-center gap-3">
{icon && React.cloneElement(icon, {
className: "w-6 h-6 text-white/90 group-hover:text-white transition-colors shrink-0",
})}
<span className="text-white font-bold text-base whitespace-nowrap">{title}</span>
</div>
{/* Arrow - Re-added as requested */}
<div className="relative z-10 opacity-70 group-hover:opacity-100 group-hover:translate-x-1 transition-all duration-300">
<svg
viewBox="0 0 24 24"
stroke="currentColor"
fill="none"
className="w-4 h-4 text-white">
<path
d="M9 5l7 7-7 7"
strokeWidth={2}
strokeLinejoin="round"
strokeLinecap="round"></path>
</svg>
</div>
</button>
);
};

View File

@ -0,0 +1,203 @@
"use client";;
import { ArrowLeft, ArrowRight } from "lucide-react";
import { Link } from "react-router-dom";
import { useEffect, useState } from "react";
import { Button } from "@/components/ui/button";
import { Carousel, CarouselContent, CarouselItem } from "@/components/ui/carousel";
const data = [
{
id: "shadcn-ui",
title: "shadcn/ui: Building a Modern Component Library",
description:
"Explore how shadcn/ui revolutionized React component libraries by providing a unique approach to component distribution and customization, making it easier for developers to build beautiful, accessible applications.",
href: "https://ui.shadcn.com",
image:
"https://images.unsplash.com/photo-1551250928-243dc937c49d?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjN8fHx8fHwyfHwxNzIzODA2OTM5fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "tailwind",
title: "Tailwind CSS: The Utility-First Revolution",
description:
"Discover how Tailwind CSS transformed the way developers style their applications, offering a utility-first approach that speeds up development while maintaining complete design flexibility.",
href: "https://tailwindcss.com",
image:
"https://images.unsplash.com/photo-1551250928-e4a05afaed1e?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjR8fHx8fHwyfHwxNzIzODA2OTM5fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "astro",
title: "Astro: The All-in-One Web Framework",
description:
"Learn how Astro's innovative 'Islands Architecture' and zero-JS-by-default approach is helping developers build faster websites while maintaining rich interactivity where needed.",
href: "https://astro.build",
image:
"https://images.unsplash.com/photo-1536735561749-fc87494598cb?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxNzd8fHx8fHwyfHwxNzIzNjM0NDc0fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "react",
title: "React: Pioneering Component-Based UI",
description:
"See how React continues to shape modern web development with its component-based architecture, enabling developers to build complex user interfaces with reusable, maintainable code.",
href: "https://react.dev",
image:
"https://images.unsplash.com/photo-1548324215-9133768e4094?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMzF8fHx8fHwyfHwxNzIzNDM1MzA1fA&ixlib=rb-4.0.3&q=80&w=1080",
},
{
id: "nextjs",
title: "Next.js: The React Framework for Production",
description:
"Explore how Next.js has become the go-to framework for building full-stack React applications, offering features like server components, file-based routing, and automatic optimization.",
href: "https://nextjs.org",
image:
"https://images.unsplash.com/photo-1550070881-a5d71eda5800?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3w2NDI3NzN8MHwxfGFsbHwxMjV8fHx8fHwyfHwxNzIzNDM1Mjk4fA&ixlib=rb-4.0.3&q=80&w=1080",
},
];
const Gallery4 = ({
title = "Case Studies",
description = "Discover how leading companies and developers are leveraging modern web technologies to build exceptional digital experiences. These case studies showcase real-world applications and success stories.",
items = data
}) => {
const [carouselApi, setCarouselApi] = useState();
const [canScrollPrev, setCanScrollPrev] = useState(false);
const [canScrollNext, setCanScrollNext] = useState(false);
const [currentSlide, setCurrentSlide] = useState(0);
useEffect(() => {
if (!carouselApi) {
return;
}
const updateSelection = () => {
setCanScrollPrev(carouselApi.canScrollPrev());
setCanScrollNext(carouselApi.canScrollNext());
setCurrentSlide(carouselApi.selectedScrollSnap());
};
updateSelection();
carouselApi.on("select", updateSelection);
return () => {
carouselApi.off("select", updateSelection);
};
}, [carouselApi]);
return (
<section className="py-32">
<div className="container mx-auto">
<div className="mb-8 flex items-end justify-between md:mb-14 lg:mb-16">
<div className="flex flex-col gap-4">
<h2 className="text-3xl font-medium md:text-4xl lg:text-5xl">
{title}
</h2>
<p className="max-w-lg text-muted-foreground">{description}</p>
</div>
<div className="hidden shrink-0 gap-2 md:flex">
<Button
size="icon"
variant="ghost"
onClick={() => {
carouselApi?.scrollPrev();
}}
disabled={!canScrollPrev}
className="disabled:pointer-events-auto">
<ArrowLeft className="size-5" />
</Button>
<Button
size="icon"
variant="ghost"
onClick={() => {
carouselApi?.scrollNext();
}}
disabled={!canScrollNext}
className="disabled:pointer-events-auto">
<ArrowRight className="size-5" />
</Button>
</div>
</div>
</div>
<div className="w-full">
<Carousel
setApi={setCarouselApi}
opts={{
align: "start",
loop: true,
breakpoints: {
"(max-width: 768px)": {
dragFree: true,
},
},
}}>
<CarouselContent
className="ml-0 2xl:ml-[max(8rem,calc(50vw-700px))] 2xl:mr-[max(0rem,calc(50vw-700px))]">
{items.map((item) => (
<CarouselItem key={item.id} className="max-w-[320px] pl-[20px] lg:max-w-[360px]">
{item.href.startsWith('/') ? (
<Link to={item.href} className="group rounded-xl">
<div
className="group relative h-full min-h-[27rem] max-w-full overflow-hidden rounded-xl md:aspect-[5/4] lg:aspect-[16/9]">
<img
src={item.image}
alt={item.title}
className="absolute h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105" />
<div
className="absolute inset-0 h-full bg-[linear-gradient(hsl(var(--primary)/0),hsl(var(--primary)/0.4),hsl(var(--primary)/0.8)_100%)] mix-blend-multiply" />
<div
className="absolute inset-x-0 bottom-0 flex flex-col items-start p-6 text-primary-foreground md:p-8">
<div className="mb-2 pt-4 text-xl font-semibold md:mb-3 md:pt-4 lg:pt-4">
{item.title}
</div>
<div className="mb-8 line-clamp-2 md:mb-12 lg:mb-9">
{item.description}
</div>
<div className="flex items-center text-sm">
Read more{" "}
<ArrowRight className="ml-2 size-5 transition-transform group-hover:translate-x-1" />
</div>
</div>
</div>
</Link>
) : (
<a href={item.href} target="_blank" rel="noopener noreferrer" className="group rounded-xl">
<div
className="group relative h-full min-h-[27rem] max-w-full overflow-hidden rounded-xl md:aspect-[5/4] lg:aspect-[16/9]">
<img
src={item.image}
alt={item.title}
className="absolute h-full w-full object-cover object-center transition-transform duration-300 group-hover:scale-105" />
<div
className="absolute inset-0 h-full bg-[linear-gradient(hsl(var(--primary)/0),hsl(var(--primary)/0.4),hsl(var(--primary)/0.8)_100%)] mix-blend-multiply" />
<div
className="absolute inset-x-0 bottom-0 flex flex-col items-start p-6 text-primary-foreground md:p-8">
<div className="mb-2 pt-4 text-xl font-semibold md:mb-3 md:pt-4 lg:pt-4">
{item.title}
</div>
<div className="mb-8 line-clamp-2 md:mb-12 lg:mb-9">
{item.description}
</div>
<div className="flex items-center text-sm">
Read more{" "}
<ArrowRight className="ml-2 size-5 transition-transform group-hover:translate-x-1" />
</div>
</div>
</div>
</a>
)}
</CarouselItem>
))}
</CarouselContent>
</Carousel>
<div className="mt-8 flex justify-center gap-2">
{items.map((_, index) => (
<button
key={index}
className={`h-2 w-2 rounded-full transition-colors ${currentSlide === index ? "bg-primary" : "bg-primary/20"
}`}
onClick={() => carouselApi?.scrollTo(index)}
aria-label={`Go to slide ${index + 1}`} />
))}
</div>
</div>
</section>
);
};
export { Gallery4 };

View File

@ -0,0 +1,251 @@
import React from 'react';
import styled from 'styled-components';
import { useTheme } from '../contexts/ThemeContext';
const Switch = () => {
const { toggleTheme, isDark } = useTheme();
const handleChange = () => {
toggleTheme();
};
return (
<StyledWrapper>
<label className="theme-switch">
<input
type="checkbox"
className="theme-switch__checkbox"
checked={isDark}
onChange={handleChange}
/>
<div className="theme-switch__container">
<div className="theme-switch__clouds" />
<div className="theme-switch__stars-container">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 144 55" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M135.831 3.00688C135.055 3.85027 134.111 4.29946 133 4.35447C134.111 4.40947 135.055 4.85867 135.831 5.71123C136.607 6.55462 136.996 7.56303 136.996 8.72727C136.996 7.95722 137.172 7.25134 137.525 6.59129C137.886 5.93124 138.372 5.39954 138.98 5.00535C139.598 4.60199 140.268 4.39114 141 4.35447C139.88 4.2903 138.936 3.85027 138.16 3.00688C137.384 2.16348 136.996 1.16425 136.996 0C136.996 1.16425 136.607 2.16348 135.831 3.00688ZM31 23.3545C32.1114 23.2995 33.0551 22.8503 33.8313 22.0069C34.6075 21.1635 34.9956 20.1642 34.9956 19C34.9956 20.1642 35.3837 21.1635 36.1599 22.0069C36.9361 22.8503 37.8798 23.2903 39 23.3545C38.2679 23.3911 37.5976 23.602 36.9802 24.0053C36.3716 24.3995 35.8864 24.9312 35.5248 25.5913C35.172 26.2513 34.9956 26.9572 34.9956 27.7273C34.9956 26.563 34.6075 25.5546 33.8313 24.7112C33.0551 23.8587 32.1114 23.4095 31 23.3545ZM0 36.3545C1.11136 36.2995 2.05513 35.8503 2.83131 35.0069C3.6075 34.1635 3.99559 33.1642 3.99559 32C3.99559 33.1642 4.38368 34.1635 5.15987 35.0069C5.93605 35.8503 6.87982 36.2903 8 36.3545C7.26792 36.3911 6.59757 36.602 5.98015 37.0053C5.37155 37.3995 4.88644 37.9312 4.52481 38.5913C4.172 39.2513 3.99559 39.9572 3.99559 40.7273C3.99559 39.563 3.6075 38.5546 2.83131 37.7112C2.05513 36.8587 1.11136 36.4095 0 36.3545ZM56.8313 24.0069C56.0551 24.8503 55.1114 25.2995 54 25.3545C55.1114 25.4095 56.0551 25.8587 56.8313 26.7112C57.6075 27.5546 57.9956 28.563 57.9956 29.7273C57.9956 28.9572 58.172 28.2513 58.5248 27.5913C58.8864 26.9312 59.3716 26.3995 59.9802 26.0053C60.5976 25.602 61.2679 25.3911 62 25.3545C60.8798 25.2903 59.9361 24.8503 59.1599 24.0069C58.3837 23.1635 57.9956 22.1642 57.9956 21C57.9956 22.1642 57.6075 23.1635 56.8313 24.0069ZM81 25.3545C82.1114 25.2995 83.0551 24.8503 83.8313 24.0069C84.6075 23.1635 84.9956 22.1642 84.9956 21C84.9956 22.1642 85.3837 23.1635 86.1599 24.0069C86.9361 24.8503 87.8798 25.2903 89 25.3545C88.2679 25.3911 87.5976 25.602 86.9802 26.0053C86.3716 26.3995 85.8864 26.9312 85.5248 27.5913C85.172 28.2513 84.9956 28.9572 84.9956 29.7273C84.9956 28.563 84.6075 27.5546 83.8313 26.7112C83.0551 25.8587 82.1114 25.4095 81 25.3545ZM136 36.3545C137.111 36.2995 138.055 35.8503 138.831 35.0069C139.607 34.1635 139.996 33.1642 139.996 32C139.996 33.1642 140.384 34.1635 141.16 35.0069C141.936 35.8503 142.88 36.2903 144 36.3545C143.268 36.3911 142.598 36.602 141.98 37.0053C141.372 37.3995 140.886 37.9312 140.525 38.5913C140.172 39.2513 139.996 39.9572 139.996 40.7273C139.996 39.563 139.607 38.5546 138.831 37.7112C138.055 36.8587 137.111 36.4095 136 36.3545ZM101.831 49.0069C101.055 49.8503 100.111 50.2995 99 50.3545C100.111 50.4095 101.055 50.8587 101.831 51.7112C102.607 52.5546 102.996 53.563 102.996 54.7273C102.996 53.9572 103.172 53.2513 103.525 52.5913C103.886 51.9312 104.372 51.3995 104.98 51.0053C105.598 50.602 106.268 50.3911 107 50.3545C105.88 50.2903 104.936 49.8503 104.16 49.0069C103.384 48.1635 102.996 47.1642 102.996 46C102.996 47.1642 102.607 48.1635 101.831 49.0069Z"
fill="currentColor" />
</svg>
</div>
<div className="theme-switch__circle-container">
<div className="theme-switch__sun-moon-container">
<div className="theme-switch__moon">
<div className="theme-switch__spot" />
<div className="theme-switch__spot" />
<div className="theme-switch__spot" />
</div>
</div>
</div>
</div>
</label>
</StyledWrapper>
);
}
const StyledWrapper = styled.div`
.theme-switch {
--toggle-size: 7px;
/* the size is adjusted using font-size,
this is not transform scale,
so you can choose any size */
--container-width: 5.625em;
--container-height: 2.5em;
--container-radius: 6.25em;
/* radius 0 - minecraft mode :) */
--container-light-bg: #3D7EAE;
--container-night-bg: #1D1F2C;
--circle-container-diameter: 3.375em;
--sun-moon-diameter: 2.125em;
--sun-bg: #ECCA2F;
--moon-bg: #C4C9D1;
--spot-color: #959DB1;
--circle-container-offset: calc((var(--circle-container-diameter) - var(--container-height)) / 2 * -1);
--stars-color: #fff;
--clouds-color: #F3FDFF;
--back-clouds-color: #AACADF;
--transition: .5s cubic-bezier(0, -0.02, 0.4, 1.25);
--circle-transition: .3s cubic-bezier(0, -0.02, 0.35, 1.17);
}
.theme-switch, .theme-switch *, .theme-switch *::before, .theme-switch *::after {
-webkit-box-sizing: border-box;
box-sizing: border-box;
margin: 0;
padding: 0;
font-size: var(--toggle-size);
}
.theme-switch__container {
width: var(--container-width);
height: var(--container-height);
background-color: var(--container-light-bg);
border-radius: var(--container-radius);
overflow: hidden;
cursor: pointer;
-webkit-box-shadow: 0em -0.062em 0.062em rgba(0, 0, 0, 0.25), 0em 0.062em 0.125em rgba(255, 255, 255, 0.94);
box-shadow: 0em -0.062em 0.062em rgba(0, 0, 0, 0.25), 0em 0.062em 0.125em rgba(255, 255, 255, 0.94);
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
position: relative;
}
.theme-switch__container::before {
content: "";
position: absolute;
z-index: 1;
inset: 0;
-webkit-box-shadow: 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset, 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset;
box-shadow: 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset, 0em 0.05em 0.187em rgba(0, 0, 0, 0.25) inset;
border-radius: var(--container-radius)
}
.theme-switch__checkbox {
display: none;
}
.theme-switch__circle-container {
width: var(--circle-container-diameter);
height: var(--circle-container-diameter);
background-color: rgba(255, 255, 255, 0.1);
position: absolute;
left: var(--circle-container-offset);
top: var(--circle-container-offset);
border-radius: var(--container-radius);
-webkit-box-shadow: inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), 0 0 0 0.625em rgba(255, 255, 255, 0.1), 0 0 0 1.25em rgba(255, 255, 255, 0.1);
box-shadow: inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), inset 0 0 0 3.375em rgba(255, 255, 255, 0.1), 0 0 0 0.625em rgba(255, 255, 255, 0.1), 0 0 0 1.25em rgba(255, 255, 255, 0.1);
display: -webkit-box;
display: -ms-flexbox;
display: flex;
-webkit-transition: var(--circle-transition);
-o-transition: var(--circle-transition);
transition: var(--circle-transition);
pointer-events: none;
}
.theme-switch__sun-moon-container {
pointer-events: auto;
position: relative;
z-index: 2;
width: var(--sun-moon-diameter);
height: var(--sun-moon-diameter);
margin: auto;
border-radius: var(--container-radius);
background-color: var(--sun-bg);
-webkit-box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #a1872a inset;
box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #a1872a inset;
-webkit-filter: drop-shadow(0.062em 0.125em 0.125em rgba(0, 0, 0, 0.25)) drop-shadow(0em 0.062em 0.125em rgba(0, 0, 0, 0.25));
filter: drop-shadow(0.062em 0.125em 0.125em rgba(0, 0, 0, 0.25)) drop-shadow(0em 0.062em 0.125em rgba(0, 0, 0, 0.25));
overflow: hidden;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
.theme-switch__moon {
-webkit-transform: translateX(100%);
-ms-transform: translateX(100%);
transform: translateX(100%);
width: 100%;
height: 100%;
background-color: var(--moon-bg);
border-radius: inherit;
-webkit-box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #969696 inset;
box-shadow: 0.062em 0.062em 0.062em 0em rgba(254, 255, 239, 0.61) inset, 0em -0.062em 0.062em 0em #969696 inset;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
position: relative;
}
.theme-switch__spot {
position: absolute;
top: 0.75em;
left: 0.312em;
width: 0.75em;
height: 0.75em;
border-radius: var(--container-radius);
background-color: var(--spot-color);
-webkit-box-shadow: 0em 0.0312em 0.062em rgba(0, 0, 0, 0.25) inset;
box-shadow: 0em 0.0312em 0.062em rgba(0, 0, 0, 0.25) inset;
}
.theme-switch__spot:nth-of-type(2) {
width: 0.375em;
height: 0.375em;
top: 0.937em;
left: 1.375em;
}
.theme-switch__spot:nth-last-of-type(3) {
width: 0.25em;
height: 0.25em;
top: 0.312em;
left: 0.812em;
}
.theme-switch__clouds {
width: 1.25em;
height: 1.25em;
background-color: var(--clouds-color);
border-radius: var(--container-radius);
position: absolute;
bottom: -0.625em;
left: 0.312em;
-webkit-box-shadow: 0.937em 0.312em var(--clouds-color), -0.312em -0.312em var(--back-clouds-color), 1.437em 0.375em var(--clouds-color), 0.5em -0.125em var(--back-clouds-color), 2.187em 0 var(--clouds-color), 1.25em -0.062em var(--back-clouds-color), 2.937em 0.312em var(--clouds-color), 2em -0.312em var(--back-clouds-color), 3.625em -0.062em var(--clouds-color), 2.625em 0em var(--back-clouds-color), 4.5em -0.312em var(--clouds-color), 3.375em -0.437em var(--back-clouds-color), 4.625em -1.75em 0 0.437em var(--clouds-color), 4em -0.625em var(--back-clouds-color), 4.125em -2.125em 0 0.437em var(--back-clouds-color);
box-shadow: 0.937em 0.312em var(--clouds-color), -0.312em -0.312em var(--back-clouds-color), 1.437em 0.375em var(--clouds-color), 0.5em -0.125em var(--back-clouds-color), 2.187em 0 var(--clouds-color), 1.25em -0.062em var(--back-clouds-color), 2.937em 0.312em var(--clouds-color), 2em -0.312em var(--back-clouds-color), 3.625em -0.062em var(--clouds-color), 2.625em 0em var(--back-clouds-color), 4.5em -0.312em var(--clouds-color), 3.375em -0.437em var(--back-clouds-color), 4.625em -1.75em 0 0.437em var(--clouds-color), 4em -0.625em var(--back-clouds-color), 4.125em -2.125em 0 0.437em var(--back-clouds-color);
-webkit-transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
-o-transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
transition: 0.5s cubic-bezier(0, -0.02, 0.4, 1.25);
}
.theme-switch__stars-container {
position: absolute;
color: var(--stars-color);
top: -100%;
left: 0.312em;
width: 2.75em;
height: auto;
-webkit-transition: var(--transition);
-o-transition: var(--transition);
transition: var(--transition);
}
/* actions */
.theme-switch__checkbox:checked + .theme-switch__container {
background-color: var(--container-night-bg);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__circle-container {
left: calc(100% - var(--circle-container-offset) - var(--circle-container-diameter));
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__circle-container:hover {
left: calc(100% - var(--circle-container-offset) - var(--circle-container-diameter) - 0.187em)
}
.theme-switch__circle-container:hover {
left: calc(var(--circle-container-offset) + 0.187em);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__moon {
-webkit-transform: translate(0);
-ms-transform: translate(0);
transform: translate(0);
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__clouds {
bottom: -4.062em;
}
.theme-switch__checkbox:checked + .theme-switch__container .theme-switch__stars-container {
top: 50%;
-webkit-transform: translateY(-50%);
-ms-transform: translateY(-50%);
transform: translateY(-50%);
}`;
export default Switch;

View File

@ -0,0 +1,369 @@
"use client";
import { TimelineContent } from "@/components/ui/timeline-animation";
import { VerticalCutReveal } from "@/components/ui/vertical-cut-reveal";
import { ArrowRight, Shield, Heart, Lock, Scale, Phone } from "lucide-react";
import { useRef, useEffect, useState } from "react";
import { useInView, motion, AnimatePresence } from "framer-motion";
export default function AboutSection() {
const heroRef = useRef(null);
const counterRef = useRef(null);
const isCounterInView = useInView(counterRef, { once: true });
const [count, setCount] = useState(0);
// Carousel state
const heroImages = ["/Gambar1.jpg", "/Gambar2.jpeg", "/Gambar3.jpeg"];
const [slideIndex, setSlideIndex] = useState(0);
const [progress, setProgress] = useState(0);
const SLIDE_DURATION = 5000; // 5 seconds
useEffect(() => {
const progressInterval = setInterval(() => {
setProgress((prev) => {
if (prev >= 100) return 0;
return prev + (100 / (SLIDE_DURATION / 50));
});
}, 50);
const slideTimer = setInterval(() => {
setSlideIndex((prev) => (prev + 1) % heroImages.length);
setProgress(0);
}, SLIDE_DURATION);
return () => {
clearInterval(progressInterval);
clearInterval(slideTimer);
};
}, []);
useEffect(() => {
if (!isCounterInView) return;
let start = 0;
const end = 100;
const duration = 2000;
const stepTime = duration / end;
const timer = setInterval(() => {
start += 1;
setCount(start);
if (start >= end) clearInterval(timer);
}, stepTime);
return () => clearInterval(timer);
}, [isCounterInView]);
const revealVariants = {
visible: (i) => ({
y: 0,
opacity: 1,
filter: "blur(0px)",
transition: {
delay: i * 0.15,
duration: 0.3,
},
}),
hidden: {
filter: "blur(10px)",
y: -20,
opacity: 0,
},
};
const scaleVariants = {
visible: (i) => ({
opacity: 1,
filter: "blur(0px)",
transition: {
delay: i * 0.15,
duration: 0.3,
},
}),
hidden: {
filter: "blur(10px)",
opacity: 0,
},
};
return (
<section className="min-h-screen py-16 bg-[#f9f9f9] flex items-center" ref={heroRef} id="about">
<div className="max-w-[90rem] mx-auto w-full px-8 lg:px-12">
<div className="relative">
{/* Header */}
<div className="flex justify-between items-center mb-8 w-[85%] absolute lg:top-4 md:top-0 sm:-top-2 -top-3 z-10">
<div className="flex items-center gap-2 text-xl">
<span className="text-[#191970] animate-spin"></span>
<TimelineContent
as="span"
animationNum={0}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-sm font-medium text-gray-600"
>
TENTANG KAMI
</TimelineContent>
</div>
<div className="flex gap-3">
<TimelineContent
as="div"
animationNum={0}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Shield className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={1}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Heart className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={2}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Lock className="w-4 h-4 text-[#191970]" />
</TimelineContent>
<TimelineContent
as="div"
animationNum={3}
timelineRef={heroRef}
customVariants={revealVariants}
className="md:w-8 md:h-8 sm:w-6 w-5 sm:h-6 h-5 border border-indigo-200 bg-indigo-50 rounded-lg flex items-center justify-center"
>
<Scale className="w-4 h-4 text-[#191970]" />
</TimelineContent>
</div>
</div>
{/* Hero Image with clip path */}
<TimelineContent
as="figure"
animationNum={4}
timelineRef={heroRef}
customVariants={scaleVariants}
className="relative group"
>
<svg
className="w-full"
width={"100%"}
height={"100%"}
viewBox="0 0 100 40"
>
<defs>
<clipPath
id="clip-inverted"
clipPathUnits={"userSpaceOnUse"}
>
<path
transform="scale(100, 40)"
d="M0.0998072 1H0.422076H0.749756C0.767072 1 0.774207 0.961783 0.77561 0.942675V0.807325C0.777053 0.743631 0.791844 0.731953 0.799059 0.734076H0.969813C0.996268 0.730255 1.00088 0.693206 0.999875 0.675159V0.0700637C0.999875 0.0254777 0.985045 0.00477707 0.977629 0H0.902473C0.854975 0 0.890448 0.138535 0.850165 0.138535H0.0204424C0.00408849 0.142357 0 0.180467 0 0.199045V0.410828C0 0.449045 0.0136283 0.46603 0.0204424 0.469745H0.0523086C0.0696245 0.471019 0.0735527 0.497877 0.0733523 0.511146V0.915605C0.0723903 0.983121 0.090588 1 0.0998072 1Z"
fill="#D9D9D9"
/>
</clipPath>
</defs>
<g clipPath="url(#clip-inverted)">
<AnimatePresence>
<motion.image
key={slideIndex}
preserveAspectRatio="xMidYMid slice"
width={"100%"}
height={"100%"}
xlinkHref={heroImages[slideIndex]}
initial={{ x: 100 }}
animate={{ x: 0 }}
exit={{ x: -100 }}
transition={{ duration: 0.6, ease: "easeInOut" }}
/>
</AnimatePresence>
</g>
</svg>
{/* Slide progress indicators */}
<div className="absolute bottom-[3%] left-[10%] flex gap-3 items-center z-10">
{heroImages.map((_, i) => (
<div
key={i}
className="h-[6px] rounded-full overflow-hidden cursor-pointer shadow-sm transition-all duration-300"
style={{
width: i === slideIndex ? '3rem' : '1.5rem',
backgroundColor: 'rgba(255,255,255,0.4)'
}}
onClick={() => { setSlideIndex(i); setProgress(0); }}
>
<div
className="h-full rounded-full transition-all duration-100 ease-linear"
style={{
width: i === slideIndex ? `${progress}%` : i < slideIndex ? '100%' : '0%',
backgroundColor: '#fff',
boxShadow: '0 0 4px rgba(0,0,0,0.2)'
}}
/>
</div>
))}
</div>
{/* 100% stat in the white cutout */}
<TimelineContent
as="div"
animationNum={5}
timelineRef={heroRef}
customVariants={revealVariants}
className="absolute bottom-[10%] right-[0.5%] text-right"
>
<div className="flex items-baseline gap-2 justify-end" ref={counterRef}>
<span className="text-[#191970] font-extrabold text-2xl sm:text-3xl lg:text-4xl leading-none" style={{ fontFamily: "'Inter', sans-serif" }}>{count}%</span>
<span className="text-gray-500 text-2xl sm:text-3xl lg:text-4xl uppercase tracking-wider font-normal leading-none">Rahasia</span>
</div>
<span className="text-gray-400 text-xs sm:text-sm mt-0.5 block text-left">kerahasiaan terjamin</span>
</TimelineContent>
</TimelineContent>
{/* Stats */}
<div className="flex flex-wrap items-center justify-start gap-6 py-4 text-sm">
<TimelineContent
as="div"
animationNum={5}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">24/7</span>
<span className="text-gray-600">layanan aktif</span>
</TimelineContent>
<span className="text-gray-300">|</span>
<TimelineContent
as="div"
animationNum={7}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">Gratis</span>
<span className="text-gray-600">tanpa biaya</span>
</TimelineContent>
<span className="text-gray-300">|</span>
<TimelineContent
as="div"
animationNum={8}
timelineRef={heroRef}
customVariants={revealVariants}
className="flex items-center gap-2 sm:text-base text-xs"
>
<span className="text-[#191970] font-bold">Cepat</span>
<span className="text-gray-600">respon tanggap</span>
</TimelineContent>
</div>
</div>
{/* Main Content */}
<div className="grid md:grid-cols-3 gap-8">
<div className="md:col-span-2">
<h2 className="sm:text-4xl md:text-5xl text-2xl !leading-[110%] font-semibold text-gray-900 mb-8">
<VerticalCutReveal
splitBy="words"
staggerDuration={0.1}
staggerFrom="first"
reverse={true}
transition={{
type: "spring",
stiffness: 250,
damping: 30,
delay: 1,
}}
>
Menciptakan Kampus Aman & Bermartabat.
</VerticalCutReveal>
</h2>
<TimelineContent
as="div"
animationNum={9}
timelineRef={heroRef}
customVariants={revealVariants}
className="grid md:grid-cols-2 gap-8 text-gray-600"
>
<TimelineContent
as="div"
animationNum={10}
timelineRef={heroRef}
customVariants={revealVariants}
className="sm:text-base text-xs"
>
<p className="leading-relaxed text-justify">
PolijeCare merupakan kanal resmi pengaduan Satgas Pencegahan
dan Penanganan Kekerasan Seksual (PPKPT) Politeknik Negeri
Jember. Kami hadir sebagai garda terdepan dalam melindungi
korban dan memberikan pendampingan profesional.
</p>
</TimelineContent>
<TimelineContent
as="div"
animationNum={11}
timelineRef={heroRef}
customVariants={revealVariants}
className="sm:text-base text-xs"
>
<p className="leading-relaxed text-justify">
Setiap laporan ditangani secara empatik, profesional, dan
menjaga kerahasiaan penuh. Kami percaya bahwa setiap individu
berhak mendapat rasa aman dalam menempuh pendidikan tanpa
ancaman kekerasan seksual.
</p>
</TimelineContent>
</TimelineContent>
</div>
<div className="md:col-span-1">
<div className="text-right">
<TimelineContent
as="div"
animationNum={12}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-[#191970] text-2xl font-bold mb-2"
>
SATGAS PPKPT
</TimelineContent>
<TimelineContent
as="div"
animationNum={13}
timelineRef={heroRef}
customVariants={revealVariants}
className="text-gray-600 text-sm mb-8"
>
Politeknik Negeri Jember
</TimelineContent>
<TimelineContent
as="div"
animationNum={14}
timelineRef={heroRef}
customVariants={revealVariants}
className="mb-6"
>
<p className="text-gray-900 font-medium mb-4">
Siap untuk melaporkan atau butuh bantuan? Kami siap mendengarkan Anda.
</p>
</TimelineContent>
<TimelineContent
as="a"
animationNum={15}
timelineRef={heroRef}
customVariants={revealVariants}
href="#contact"
className="group relative inline-flex w-fit ml-auto items-center gap-2 px-7 py-3 bg-[#191970] text-white rounded-full font-semibold text-sm cursor-pointer transition-all duration-300 hover:bg-[#1a237e] hover:text-white hover:shadow-[0_4px_20px_rgba(26,35,126,0.5)] hover:gap-3"
>
<Phone className="w-4 h-4 opacity-60" />
HUBUNGI KAMI
<ArrowRight className="w-4 h-4 transition-transform duration-300 group-hover:translate-x-1" />
</TimelineContent>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,47 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
const Button = React.forwardRef(({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props} />
);
})
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,50 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("rounded-lg border bg-card text-card-foreground shadow-sm", className)}
{...props} />
))
Card.displayName = "Card"
const CardHeader = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props} />
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn("text-2xl font-semibold leading-none tracking-tight", className)}
{...props} />
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props} />
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props} />
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,193 @@
import * as React from "react"
import useEmblaCarousel from "embla-carousel-react";
import { ArrowLeft, ArrowRight } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
const CarouselContext = React.createContext(null)
function useCarousel() {
const context = React.useContext(CarouselContext)
if (!context) {
throw new Error("useCarousel must be used within a <Carousel />")
}
return context
}
const Carousel = React.forwardRef((
{
orientation = "horizontal",
opts,
setApi,
plugins,
className,
children,
...props
},
ref,
) => {
const [carouselRef, api] = useEmblaCarousel({
...opts,
axis: orientation === "horizontal" ? "x" : "y",
}, plugins)
const [canScrollPrev, setCanScrollPrev] = React.useState(false)
const [canScrollNext, setCanScrollNext] = React.useState(false)
const onSelect = React.useCallback((api) => {
if (!api) {
return
}
setCanScrollPrev(api.canScrollPrev())
setCanScrollNext(api.canScrollNext())
}, [])
const scrollPrev = React.useCallback(() => {
api?.scrollPrev()
}, [api])
const scrollNext = React.useCallback(() => {
api?.scrollNext()
}, [api])
const handleKeyDown = React.useCallback((event) => {
if (event.key === "ArrowLeft") {
event.preventDefault()
scrollPrev()
} else if (event.key === "ArrowRight") {
event.preventDefault()
scrollNext()
}
}, [scrollPrev, scrollNext])
React.useEffect(() => {
if (!api || !setApi) {
return
}
setApi(api)
}, [api, setApi])
React.useEffect(() => {
if (!api) {
return
}
onSelect(api)
api.on("reInit", onSelect)
api.on("select", onSelect)
return () => {
api?.off("select", onSelect)
};
}, [api, onSelect])
return (
<CarouselContext.Provider
value={{
carouselRef,
api: api,
opts,
orientation:
orientation || (opts?.axis === "y" ? "vertical" : "horizontal"),
scrollPrev,
scrollNext,
canScrollPrev,
canScrollNext,
}}>
<div
ref={ref}
onKeyDownCapture={handleKeyDown}
className={cn("relative", className)}
role="region"
aria-roledescription="carousel"
{...props}>
{children}
</div>
</CarouselContext.Provider>
);
})
Carousel.displayName = "Carousel"
const CarouselContent = React.forwardRef(({ className, ...props }, ref) => {
const { carouselRef, orientation } = useCarousel()
return (
<div ref={carouselRef} className="overflow-hidden">
<div
ref={ref}
className={cn(
"flex",
orientation === "horizontal" ? "-ml-4" : "-mt-4 flex-col",
className
)}
{...props} />
</div>
);
})
CarouselContent.displayName = "CarouselContent"
const CarouselItem = React.forwardRef(({ className, ...props }, ref) => {
const { orientation } = useCarousel()
return (
<div
ref={ref}
role="group"
aria-roledescription="slide"
className={cn(
"min-w-0 shrink-0 grow-0 basis-full",
orientation === "horizontal" ? "pl-4" : "pt-4",
className
)}
{...props} />
);
})
CarouselItem.displayName = "CarouselItem"
const CarouselPrevious = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollPrev, canScrollPrev } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-left-12 top-1/2 -translate-y-1/2"
: "-top-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollPrev}
onClick={scrollPrev}
{...props}>
<ArrowLeft className="h-4 w-4" />
<span className="sr-only">Previous slide</span>
</Button>
);
})
CarouselPrevious.displayName = "CarouselPrevious"
const CarouselNext = React.forwardRef(({ className, variant = "outline", size = "icon", ...props }, ref) => {
const { orientation, scrollNext, canScrollNext } = useCarousel()
return (
<Button
ref={ref}
variant={variant}
size={size}
className={cn("absolute h-8 w-8 rounded-full", orientation === "horizontal"
? "-right-12 top-1/2 -translate-y-1/2"
: "-bottom-12 left-1/2 -translate-x-1/2 rotate-90", className)}
disabled={!canScrollNext}
onClick={scrollNext}
{...props}>
<ArrowRight className="h-4 w-4" />
<span className="sr-only">Next slide</span>
</Button>
);
})
CarouselNext.displayName = "CarouselNext"
export { Carousel, CarouselContent, CarouselItem, CarouselPrevious, CarouselNext };

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { Check } from "lucide-react"
import { cn } from "@/lib/utils"
const Checkbox = React.forwardRef(({ className, ...props }, ref) => (
<CheckboxPrimitive.Root
ref={ref}
className={cn(
"peer h-4 w-4 shrink-0 rounded-sm border border-primary ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground",
className
)}
{...props}>
<CheckboxPrimitive.Indicator className={cn("flex items-center justify-center text-current")}>
<Check className="h-4 w-4" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
))
Checkbox.displayName = CheckboxPrimitive.Root.displayName
export { Checkbox }

View File

@ -0,0 +1,120 @@
"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: (isExpanded) => ({
gap: isExpanded ? ".5rem" : 0,
paddingLeft: isExpanded ? "2rem" : "1rem",
paddingRight: isExpanded ? "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 [hovered, setHovered] = React.useState(null);
const outsideClickRef = React.useRef(null);
React.useEffect(() => {
if (activeTab !== undefined && activeTab !== null) {
setSelected(activeTab);
}
}, [activeTab]);
useOnClickOutside(outsideClickRef, () => {
// Keep the active tab visible don't clear selection on outside click
});
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;
const isExpanded = selected === index || hovered === index;
return (
<motion.button
key={tab.title}
layout
variants={buttonVariants}
initial={false}
animate="animate"
custom={isExpanded}
onClick={() => handleSelect(index)}
onMouseEnter={() => setHovered(index)}
onMouseLeave={() => setHovered(null)}
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)]"
: hovered === index
? "bg-gray-200 text-foreground"
: "text-muted-foreground hover:text-foreground"
)}>
<Icon size={24} />
<AnimatePresence initial={false}>
{isExpanded && (
<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>
);
}

View File

@ -0,0 +1,53 @@
// This is file of your component
// You can use any dependencies from npm; we import them automatically in package.json
'use client';
import { ArrowRight } from 'lucide-react';
export function FlowButton({
text = "Button",
href,
target,
rel,
className = "",
colorStr = "#111111", // Default text/border color
hoverColorStr = "#111111", // Circle bg color on hover
}) {
const Component = href ? 'a' : 'button';
return (
<Component
href={href}
target={target}
rel={rel}
className={`group relative flex items-center justify-center gap-1 overflow-hidden rounded-[100px] border-[1.5px] bg-transparent px-8 py-3 text-sm font-semibold cursor-pointer transition-all duration-[600ms] ease-[cubic-bezier(0.23,1,0.32,1)] hover:border-transparent hover:!text-white hover:rounded-[12px] active:scale-[0.95] ${className}`}
style={{
borderColor: `${colorStr}40`, // 40 is hex opacity
color: colorStr
}}
>
{/* Left arrow */}
<ArrowRight
className="absolute w-4 h-4 left-[-25%] fill-none z-[9] group-hover:left-4 group-hover:!stroke-white transition-all duration-[800ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
style={{ stroke: colorStr }}
/>
{/* Text */}
<span className="relative z-[1] -translate-x-3 group-hover:translate-x-3 transition-all duration-[800ms] ease-out group-hover:!text-white">
{text}
</span>
{/* Circle Hover Effect */}
<span
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-4 h-4 rounded-[50%] opacity-0 group-hover:w-[300px] group-hover:h-[300px] group-hover:opacity-100 transition-all duration-[800ms] ease-[cubic-bezier(0.19,1,0.22,1)]"
style={{ backgroundColor: hoverColorStr }}
></span>
{/* Right arrow */}
<ArrowRight
className="absolute w-4 h-4 right-4 fill-none z-[9] group-hover:right-[-25%] group-hover:!stroke-white transition-all duration-[800ms] ease-[cubic-bezier(0.34,1.56,0.64,1)]"
style={{ stroke: colorStr }}
/>
</Component>
);
}

View File

@ -0,0 +1,19 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Input = React.forwardRef(({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props} />
);
})
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,16 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva } from "class-variance-authority";
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef(({ className, ...props }, ref) => (
<LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,92 @@
import { useState, useRef, useEffect } from "react";
import { motion, AnimatePresence, useScroll, useMotionValueEvent } from "framer-motion";
import { Plus, Heart, Star } from "lucide-react";
export function ScrollParticles() {
const { scrollY } = useScroll();
const [particles, setParticles] = useState([]);
const [windowHeight, setWindowHeight] = useState(0);
const lastY = useRef(0);
useEffect(() => {
if (typeof window !== "undefined") {
setWindowHeight(window.innerHeight);
const handleResize = () => setWindowHeight(window.innerHeight);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
}
}, []);
useMotionValueEvent(scrollY, "change", (latest) => {
const diff = Math.abs(latest - lastY.current);
const direction = latest > lastY.current ? 1 : -1;
// Spawn particles if scrolling fast enough
if (diff > 5 && windowHeight > 0) {
const scrollHeight = document.documentElement.scrollHeight - windowHeight;
const progress = latest / scrollHeight;
// Calculate approximate scrollbar thumb position (center of thumb)
const thumbY = progress * (windowHeight - 40);
const shapes = ["plus", "heart", "star"];
const randomShape = shapes[Math.floor(Math.random() * shapes.length)];
const newParticle = {
id: Date.now() + Math.random(),
top: thumbY + (Math.random() * 40 - 20), // Wider spread
left: -Math.random() * 20 - 10,
size: Math.random() * 14 + 10, // Larger size: 10px - 24px
color: Math.random() > 0.5 ? "#191970" : "#4C6EF5",
shape: randomShape,
rotation: Math.random() * 360,
velocity: {
x: -Math.random() * 30 - 20, // Faster drift left
y: (Math.random() * 30 - 15) + (direction * 8)
}
};
setParticles(prev => [...prev.slice(-20), newParticle]);
}
lastY.current = latest;
});
return (
<div className="fixed top-0 right-0 w-4 h-full pointer-events-none z-[9999]">
<AnimatePresence>
{particles.map((particle) => (
<motion.div
key={particle.id}
initial={{
opacity: 1,
x: 0,
y: particle.top,
scale: 0,
rotate: particle.rotation
}}
animate={{
opacity: 0,
x: particle.velocity.x,
y: particle.top + particle.velocity.y,
scale: 1,
rotate: particle.rotation + 180
}}
exit={{ opacity: 0 }}
transition={{ duration: 0.8, ease: "easeOut" }}
className="absolute right-1 flex items-center justify-center"
style={{
width: particle.size,
height: particle.size,
color: particle.color,
}}
>
{particle.shape === "plus" && <Plus size={particle.size} strokeWidth={3} />}
{particle.shape === "heart" && <Heart size={particle.size} fill={particle.color} strokeWidth={0} />}
{particle.shape === "star" && <Star size={particle.size} fill={particle.color} strokeWidth={0} />}
</motion.div>
))}
</AnimatePresence>
</div>
);
}

View File

@ -0,0 +1,120 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn("flex cursor-default items-center justify-center py-1", className)}
{...props}>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn("p-1", position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]")}>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props} />
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props} />
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,43 @@
import React, { useRef, useState } from "react";
export const SpotlightCard = ({ children, className = "", spotlightColor = "rgba(255, 255, 255, 0.25)" }) => {
const divRef = useRef(null);
const [position, setPosition] = useState({ x: 0, y: 0 });
const [opacity, setOpacity] = useState(0);
const handleMouseMove = (e) => {
if (!divRef.current) return;
const rect = divRef.current.getBoundingClientRect();
setPosition({ x: e.clientX - rect.left, y: e.clientY - rect.top });
};
const handleMouseEnter = () => {
setOpacity(1);
};
const handleMouseLeave = () => {
setOpacity(0);
};
return (
<div
ref={divRef}
onMouseMove={handleMouseMove}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
className={`relative overflow-hidden rounded-3xl border border-gray-100 dark:border-gray-800 shadow-lg ${className}`}
>
<div
className="pointer-events-none absolute -inset-px transition duration-300"
style={{
opacity,
background: `radial-gradient(600px circle at ${position.x}px ${position.y}px, ${spotlightColor}, transparent 40%)`,
}}
/>
<div className="relative h-full">
{children}
</div>
</div>
);
};

View File

@ -0,0 +1,24 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)} />
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,65 @@
"use client";
import React, { useRef, createElement, forwardRef } from "react";
import {
motion,
useInView,
useScroll,
useTransform,
} from "framer-motion";
import { cn } from "@/lib/utils";
const TimelineContent = forwardRef(
(
{
as = "div",
children,
className,
animationNum = 0,
timelineRef,
customVariants,
...props
},
ref
) => {
const defaultVariants = {
hidden: {
opacity: 0,
y: 20,
filter: "blur(8px)",
},
visible: (i) => ({
opacity: 1,
y: 0,
filter: "blur(0px)",
transition: {
delay: i * 0.3,
duration: 0.5,
ease: "easeOut",
},
}),
};
const variants = customVariants || defaultVariants;
const isInView = useInView(timelineRef, { once: true, amount: 0.1 });
const MotionComponent = motion[as] || motion.div;
return (
<MotionComponent
ref={ref}
className={cn(className)}
initial="hidden"
animate={isInView ? "visible" : "hidden"}
variants={variants}
custom={animationNum}
{...props}
>
{children}
</MotionComponent>
);
}
);
TimelineContent.displayName = "TimelineContent";
export { TimelineContent };

View File

@ -0,0 +1,189 @@
"use client";
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState,
} from "react";
import { motion } from "framer-motion";
import { cn } from "@/lib/utils";
const VerticalCutReveal = forwardRef(
(
{
children,
reverse = false,
transition = {
type: "spring",
stiffness: 190,
damping: 22,
},
splitBy = "words",
staggerDuration = 0.2,
staggerFrom = "first",
containerClassName,
wordLevelClassName,
elementLevelClassName,
onClick,
onStart,
onComplete,
autoStart = true,
...props
},
ref
) => {
const containerRef = useRef(null);
const text =
typeof children === "string" ? children : children?.toString() || "";
const [isAnimating, setIsAnimating] = useState(false);
const splitIntoCharacters = (text) => {
if (typeof Intl !== "undefined" && "Segmenter" in Intl) {
const segmenter = new Intl.Segmenter("en", { granularity: "grapheme" });
return Array.from(segmenter.segment(text), ({ segment }) => segment);
}
return Array.from(text);
};
const elements = useMemo(() => {
const words = text.split(" ");
if (splitBy === "characters") {
return words.map((word, i) => ({
characters: splitIntoCharacters(word),
needsSpace: i !== words.length - 1,
}));
}
return splitBy === "words"
? text.split(" ")
: splitBy === "lines"
? text.split("\n")
: text.split(splitBy);
}, [text, splitBy]);
const getStaggerDelay = useCallback(
(index) => {
const total =
splitBy === "characters"
? elements.reduce(
(acc, word) =>
acc +
(typeof word === "string"
? 1
: word.characters.length + (word.needsSpace ? 1 : 0)),
0
)
: elements.length;
if (staggerFrom === "first") return index * staggerDuration;
if (staggerFrom === "last")
return (total - 1 - index) * staggerDuration;
if (staggerFrom === "center") {
const center = Math.floor(total / 2);
return Math.abs(center - index) * staggerDuration;
}
if (staggerFrom === "random") {
const randomIndex = Math.floor(Math.random() * total);
return Math.abs(randomIndex - index) * staggerDuration;
}
return Math.abs(staggerFrom - index) * staggerDuration;
},
[elements.length, staggerFrom, staggerDuration]
);
const startAnimation = useCallback(() => {
setIsAnimating(true);
onStart?.();
}, [onStart]);
useImperativeHandle(ref, () => ({
startAnimation,
reset: () => setIsAnimating(false),
}));
useEffect(() => {
if (autoStart) {
startAnimation();
}
}, [autoStart]);
const variants = {
hidden: { y: reverse ? "-100%" : "100%" },
visible: (i) => ({
y: 0,
transition: {
...transition,
delay: (transition?.delay || 0) + getStaggerDelay(i),
},
}),
};
return (
<span
className={cn(
containerClassName,
"flex flex-wrap whitespace-pre-wrap",
splitBy === "lines" && "flex-col"
)}
onClick={onClick}
ref={containerRef}
{...props}
>
<span className="sr-only">{text}</span>
{(splitBy === "characters"
? elements
: elements.map((el, i) => ({
characters: [el],
needsSpace: i !== elements.length - 1,
}))
).map((wordObj, wordIndex, array) => {
const previousCharsCount = array
.slice(0, wordIndex)
.reduce((sum, word) => sum + word.characters.length, 0);
return (
<span
key={wordIndex}
aria-hidden="true"
className={cn("inline-flex overflow-hidden", wordLevelClassName)}
>
{wordObj.characters.map((char, charIndex) => (
<span
className={cn(
elementLevelClassName,
"whitespace-pre-wrap relative"
)}
key={charIndex}
>
<motion.span
custom={previousCharsCount + charIndex}
initial="hidden"
animate={isAnimating ? "visible" : "hidden"}
variants={variants}
onAnimationComplete={
wordIndex === elements.length - 1 &&
charIndex === wordObj.characters.length - 1
? onComplete
: undefined
}
className="inline-block"
>
{char}
</motion.span>
</span>
))}
{wordObj.needsSpace && <span> </span>}
</span>
);
})}
</span>
);
}
);
VerticalCutReveal.displayName = "VerticalCutReveal";
export { VerticalCutReveal };

View File

@ -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,138 @@ 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));
}
}
/* Blue Highlighter / Stabilo Effect - Per-letter animated */
.highlight-marker {
position: relative;
display: inline;
padding: 2px 6px;
border-radius: 3px;
font-weight: 600;
color: transparent;
background: linear-gradient(to right, #ffffff 50%, #374151 50%);
background-size: 200% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
animation: marker-text-reveal 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--delay, 0) * 1s);
}
.highlight-marker::before {
content: '';
position: absolute;
left: 0;
bottom: 0;
width: 0;
height: 100%;
background: rgba(25, 25, 112, 0.85);
border-radius: 3px;
z-index: -1;
animation: marker-sweep 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
animation-delay: calc(var(--delay, 0) * 1s);
}
@keyframes marker-sweep {
0% {
width: 0;
}
100% {
width: 100%;
}
}
@keyframes marker-text-reveal {
0% {
background-position: 100% 0;
}
100% {
background-position: 0% 0;
}
}
/* Dark mode */
.dark .highlight-marker {
background: linear-gradient(to right, #ffffff 50%, #d1d5db 50%);
background-size: 200% 100%;
background-position: 100% 0;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.dark .highlight-marker::before {
background: rgba(80, 100, 220, 0.85);
}
/* Custom Scrollbar */
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
.dark ::-webkit-scrollbar-track {
background: #1f2937;
}
::-webkit-scrollbar-thumb {
background: #191970;
border-radius: 5px;
}
::-webkit-scrollbar-thumb:hover {
background: #101050;
}
/* Firefox Scrollbar */
@supports (scrollbar-color: auto) {
html {
scrollbar-width: thin;
scrollbar-color: #191970 #f1f1f1;
}
.dark html {
scrollbar-color: #191970 #1f2937;
}
}

View File

@ -0,0 +1,6 @@
import { clsx } from "clsx";
import { twMerge } from "tailwind-merge"
export function cn(...inputs) {
return twMerge(clsx(inputs));
}

View File

@ -17,9 +17,9 @@ const LandingPage = () => {
const mockHeroData = {
title: 'Aman Bicara, Aman Melapor',
subtitle: 'Satgas PPKPT Politeknik Negeri Jember',
description: 'Kami siap mendengar dan membantu Anda dengan profesionalisme dan kerahasiaan terjamin. Setiap laporan akan ditangani dengan empati dan seksama.'
description: null
};
setHeroData(mockHeroData);
setLoading(false);
}, []);

View File

@ -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")],
}

View File

@ -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"),
},
},
})