ui update

This commit is contained in:
azgrey
2026-06-12 11:23:38 +07:00
parent 93b7b5c3d6
commit 231c074ca5
+489 -275
View File
@@ -428,34 +428,26 @@ function useToast() {
return [toast, show];
}
// ─── Landing Page (redesigned) ────────────────────────────────────────────────
// ─── Landing Page — Editorial "Puls" style ────────────────────────────────────
const LP = {
bg: "#F5F6F8",
bgHero: "#FFFFFF",
bg: "#F7F6F3",
surface: "#FFFFFF",
surfaceHover: "#F0F4FF",
border: "rgba(0,0,0,0.08)",
label: "#0F1724",
labelSub: "#5A6474",
labelMuted: "#9BA5B4",
accent: "#2563EB",
accentLight: "rgba(37,99,235,0.08)",
accentBorder: "rgba(37,99,235,0.2)",
amber: "#D97706",
amberLight: "rgba(217,119,6,0.08)",
blue: "#2563EB",
blueLight: "rgba(37,99,235,0.08)",
red: "#DC2626",
redLight: "rgba(220,38,38,0.08)",
green: "#059669",
greenLight: "rgba(5,150,105,0.08)",
separator: "rgba(0,0,0,0.06)",
label: "#0A0A0A",
labelSub: "#5C5C5C",
labelMuted: "#A0A0A0",
accent: "#E8334A", // coral-red accent
accentWarm: "#FF6B35", // warm orange for variety
border: "rgba(0,0,0,0.09)",
g1: ["#FF9500", "#FF6B00"],
g2: ["#007AFF", "#0040DD"],
g3: ["#E8334A", "#B01E31"],
g4: ["#34C759", "#248A3D"],
};
// SVG icon components for each project
function IconPin() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/>
<circle cx="12" cy="10" r="3"/>
</svg>
@@ -463,14 +455,15 @@ function IconPin() {
}
function IconRoad() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 17l3-10 3 4 3-8 3 4 3-10"/>
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/>
<path d="M13 13l6 6"/>
</svg>
);
}
function IconUsers() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
@@ -480,7 +473,7 @@ function IconUsers() {
}
function IconLayers() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round">
<polygon points="12 2 2 7 12 12 22 7 12 2"/>
<polyline points="2 17 12 22 22 17"/>
<polyline points="2 12 12 17 22 12"/>
@@ -495,17 +488,19 @@ const projects = [
title: "SPBU Map",
subtitle: "Point Layer",
description: "Tambah, hapus, dan geser marker SPBU. Data tersimpan ke MySQL.",
accent: LP.amber,
accentLight: LP.amberLight,
gradient: LP.g1,
gradId: "g1",
tag: "Titik",
},
{
id: "p02",
Icon: IconRoad,
title: "WebGIS Jalan & Tanah",
title: "Jalan & Tanah",
subtitle: "Vector Edit",
description: "Gambar dan edit polyline jalan serta polygon parsil tanah. CRUD penuh.",
accent: LP.blue,
accentLight: LP.blueLight,
gradient: LP.g2,
gradId: "g2",
tag: "Vektor",
},
{
id: "p03",
@@ -513,349 +508,568 @@ const projects = [
title: "Kepadatan Penduduk",
subtitle: "Choropleth",
description: "Koropleth kepadatan per kecamatan dengan tiga kelas warna. Data BPS.",
accent: LP.red,
accentLight: LP.redLight,
gradient: LP.g3,
gradId: "g3",
tag: "Analisis",
},
{
id: "p04",
Icon: IconLayers,
title: "SPBU + Layer Toggle",
title: "SPBU Layer Toggle",
subtitle: "Layer Control",
description: "Kontrol layer toggle untuk memisahkan SPBU berdasarkan jam operasi.",
accent: LP.green,
accentLight: LP.greenLight,
gradient: LP.g4,
gradId: "g4",
tag: "Layer",
},
];
const landingCss = `
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@700;800&family=DM+Sans:wght@300;400;500;600&display=swap');
@import url('https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=Inter:wght@300;400;500;600;700&display=swap');
@keyframes lpFadeUp {
from { opacity: 0; transform: translateY(12px); }
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes lpFadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes lpSlideLeft {
from { opacity: 0; transform: translateX(20px); }
to { opacity: 1; transform: translateX(0); }
}
.lp-root {
min-height: 100vh;
background: #F5F6F8;
font-family: 'DM Sans', -apple-system, sans-serif;
background: #F7F6F3;
font-family: 'Inter', -apple-system, 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased;
overflow-x: hidden;
color: #0A0A0A;
}
/* ── hero ── */
/* ─── NAV ─── */
.lp-nav {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
height: 56px;
position: sticky; top: 0; z-index: 200;
background: rgba(247,246,243,0.88);
backdrop-filter: blur(20px) saturate(160%);
-webkit-backdrop-filter: blur(20px) saturate(160%);
border-bottom: 1px solid rgba(0,0,0,0.07);
}
.lp-nav-left {
display: flex;
align-items: center;
gap: 4px;
}
.lp-nav-item {
padding: 6px 14px;
border-radius: 100px;
font-size: 13px;
font-weight: 500;
color: #5C5C5C;
cursor: pointer;
transition: background 0.15s, color 0.15s;
border: none;
background: transparent;
font-family: inherit;
}
.lp-nav-item:hover { background: rgba(0,0,0,0.05); color: #0A0A0A; }
.lp-nav-item.active {
background: #0A0A0A;
color: #F7F6F3;
}
.lp-nav-logo {
font-size: 15px;
font-weight: 700;
letter-spacing: -0.3px;
color: #0A0A0A;
}
.lp-nav-cta {
display: flex;
align-items: center;
gap: 6px;
padding: 7px 16px;
background: #0A0A0A;
color: #F7F6F3;
border-radius: 100px;
font-size: 13px;
font-weight: 600;
cursor: pointer;
border: none;
font-family: inherit;
transition: opacity 0.15s;
}
.lp-nav-cta:hover { opacity: 0.82; }
/* ─── HERO SPLIT ─── */
.lp-hero {
background: #FFFFFF;
border-bottom: 1px solid rgba(0,0,0,0.06);
padding: 72px 24px 60px;
display: grid;
grid-template-columns: 1fr 1fr;
min-height: 100vh;
max-height: 900px;
}
/* LEFT PANEL */
.lp-hero-left {
position: relative;
padding: 40px 28px 32px;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
position: relative;
border-right: 1px solid rgba(0,0,0,0.07);
overflow: hidden;
animation: lpFadeUp 0.5s 0.05s both;
}
/* Subtle dot grid — static, no animation */
.lp-hero::before {
/* Subtle grid lines — cartographic feel */
.lp-hero-left::before {
content: '';
position: absolute; inset: 0;
background-image: radial-gradient(circle, rgba(37,99,235,0.08) 1px, transparent 1px);
background-size: 28px 28px;
pointer-events: none;
}
/* Soft vignette over dot grid */
.lp-hero::after {
content: '';
position: absolute; inset: 0;
background: radial-gradient(ellipse 70% 80% at 50% 50%, rgba(255,255,255,0.0) 40%, rgba(255,255,255,0.95) 100%);
background-image:
linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px),
linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px);
background-size: 40px 40px;
pointer-events: none;
}
/* All hero content sits above pseudo-elements */
.lp-hero > * { position: relative; z-index: 1; }
/* ── eyebrow ── */
.lp-eyebrow {
.lp-left-eyebrow {
display: inline-flex;
align-items: center;
gap: 7px;
background: rgba(37,99,235,0.06);
border: 1px solid rgba(37,99,235,0.18);
background: #FFFFFF;
border: 1px solid rgba(0,0,0,0.10);
border-radius: 100px;
padding: 5px 14px;
padding: 5px 12px 5px 8px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.11em;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #2563EB;
margin-bottom: 22px;
animation: lpFadeUp 0.45s 0.05s both;
color: #5C5C5C;
width: fit-content;
margin-bottom: 28px;
position: relative; z-index: 1;
}
.lp-eyebrow-dot {
.lp-eyebrow-pip {
width: 6px; height: 6px;
border-radius: 50%;
background: #2563EB;
background: #E8334A;
flex-shrink: 0;
}
/* ── headline ── */
.lp-headline {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(36px, 7vw, 64px);
font-weight: 800;
line-height: 1.05;
letter-spacing: -0.02em;
color: #0F1724;
margin-bottom: 16px;
animation: lpFadeUp 0.45s 0.12s both;
}
.lp-headline em {
font-style: normal;
color: #2563EB;
}
/* ── subhead ── */
.lp-sub {
font-size: 15px;
.lp-left-heading {
font-family: 'Instrument Serif', Georgia, serif;
font-size: clamp(38px, 5.5vw, 58px);
font-weight: 400;
color: #5A6474;
max-width: 360px;
line-height: 1.65;
margin-bottom: 28px;
animation: lpFadeUp 0.45s 0.18s both;
line-height: 1.06;
letter-spacing: -0.02em;
color: #0A0A0A;
margin-bottom: 0;
position: relative; z-index: 1;
flex: 1;
}
/* ── coordinates strip ── */
.lp-coords {
display: flex;
gap: 14px;
align-items: center;
animation: lpFadeUp 0.45s 0.24s both;
.lp-left-heading .accent {
color: #E8334A;
font-style: italic;
}
.lp-coord-item {
font-size: 11px;
font-weight: 500;
letter-spacing: 0.06em;
color: #9BA5B4;
font-variant-numeric: tabular-nums;
}
.lp-coord-sep {
width: 1px; height: 10px;
background: rgba(0,0,0,0.12);
}
/* ── cards ── */
.lp-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 12px;
width: 100%;
max-width: 660px;
margin: 0 auto;
padding: 32px 20px 0;
animation: lpFadeUp 0.45s 0.28s both;
}
@media (max-width: 500px) {
.lp-cards { grid-template-columns: 1fr; }
}
.lp-card {
.lp-left-heading .circle-wrap {
display: inline-block;
position: relative;
background: #FFFFFF;
border: 1px solid rgba(0,0,0,0.08);
}
.lp-left-heading .circle-wrap svg {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
width: calc(100% + 16px);
height: calc(100% + 12px);
pointer-events: none;
}
/* Floating project mini-cards */
.lp-floats {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-top: 28px;
position: relative; z-index: 1;
}
.lp-float-card {
background: rgba(255,255,255,0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(0,0,0,0.09);
border-radius: 14px;
padding: 20px 18px 42px;
padding: 14px;
cursor: pointer;
transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s, background 0.18s;
text-align: left;
transition: transform 0.18s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.18s;
position: relative;
overflow: hidden;
}
.lp-card:hover {
transform: translateY(-2px);
box-shadow: 0 8px 28px rgba(0,0,0,0.08);
.lp-float-card:hover {
transform: translateY(-3px) scale(1.015);
box-shadow: 0 12px 32px rgba(0,0,0,0.10);
}
.lp-card-icon-wrap {
width: 40px; height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 14px;
transition: background 0.18s;
}
.lp-card-title {
font-size: 14px;
font-weight: 700;
color: #0F1724;
letter-spacing: -0.01em;
margin-bottom: 2px;
}
.lp-card-subtitle {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.09em;
margin-bottom: 9px;
transition: color 0.18s;
}
.lp-card-desc {
font-size: 12.5px;
color: #5A6474;
line-height: 1.55;
}
/* Arrow — bottom-right, slides in on hover */
.lp-card-arrow {
.lp-float-card:active { transform: scale(0.97); }
.lp-float-arrow {
position: absolute;
bottom: 16px; right: 16px;
opacity: 0;
transition: opacity 0.18s, transform 0.18s;
transform: translateX(-6px);
top: 10px; right: 10px;
width: 22px; height: 22px;
border-radius: 50%;
background: #0A0A0A;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
}
.lp-card:hover .lp-card-arrow {
opacity: 1;
transform: translateX(0);
.lp-float-tag {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: #A0A0A0;
margin-bottom: 5px;
}
.lp-float-title {
font-size: 13px;
font-weight: 600;
color: #0A0A0A;
letter-spacing: -0.2px;
line-height: 1.3;
padding-right: 24px;
}
.lp-float-desc {
font-size: 11px;
color: #5C5C5C;
line-height: 1.45;
margin-top: 5px;
}
.lp-float-icon {
width: 32px; height: 32px;
border-radius: 9px;
display: flex; align-items: center; justify-content: center;
margin-bottom: 10px;
flex-shrink: 0;
}
/* ── divider ── */
.lp-divider {
width: 100%;
max-width: 660px;
margin: 28px auto 0;
padding: 0 20px;
/* Coords bar bottom-left */
.lp-coords-bar {
display: flex;
align-items: center;
gap: 14px;
gap: 12px;
margin-top: 20px;
position: relative; z-index: 1;
}
.lp-divider-line {
flex: 1; height: 1px;
background: rgba(0,0,0,0.07);
}
.lp-divider-text {
.lp-coord-chip {
font-size: 10px;
font-weight: 500;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #9BA5B4;
white-space: nowrap;
letter-spacing: 0.05em;
color: #A0A0A0;
font-variant-numeric: tabular-nums;
}
.lp-coord-div {
width: 1px; height: 8px;
background: rgba(0,0,0,0.15);
}
/* ── footer ── */
.lp-footer {
/* RIGHT PANEL */
.lp-hero-right {
padding: 48px 36px 40px;
display: flex;
flex-direction: column;
justify-content: center;
animation: lpSlideLeft 0.5s 0.15s both;
}
.lp-right-tag {
display: inline-flex;
align-items: center;
border: 1px solid rgba(0,0,0,0.14);
border-radius: 100px;
padding: 4px 12px;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.04em;
color: #5C5C5C;
width: fit-content;
margin-bottom: 24px;
}
.lp-right-heading {
font-family: 'Instrument Serif', Georgia, serif;
font-size: clamp(34px, 4.5vw, 52px);
font-weight: 400;
line-height: 1.08;
letter-spacing: -0.02em;
color: #0A0A0A;
margin-bottom: 20px;
}
.lp-right-heading .accent { color: #E8334A; font-style: italic; }
.lp-right-heading .circle-word {
display: inline-block;
position: relative;
padding: 0 6px;
}
.lp-right-heading .circle-word::after {
content: '';
position: absolute;
inset: -1px 0;
border: 1.5px solid #0A0A0A;
border-radius: 100px;
}
.lp-right-body {
font-size: 14px;
font-weight: 400;
color: #5C5C5C;
line-height: 1.65;
max-width: 340px;
margin-bottom: 32px;
}
.lp-right-cta {
display: inline-flex;
align-items: center;
gap: 8px;
background: #E8334A;
color: #FFFFFF;
border-radius: 100px;
padding: 12px 22px;
font-size: 14px;
font-weight: 600;
cursor: pointer;
border: none;
font-family: inherit;
width: fit-content;
transition: opacity 0.15s, transform 0.15s;
letter-spacing: -0.1px;
}
.lp-right-cta:hover { opacity: 0.88; transform: translateY(-1px); }
.lp-right-cta:active { transform: scale(0.97); }
.lp-cta-arrow {
width: 22px; height: 22px;
border-radius: 50%;
background: rgba(255,255,255,0.22);
display: flex; align-items: center; justify-content: center;
}
/* Stats below CTA */
.lp-right-stats {
display: flex;
gap: 28px;
margin-top: 40px;
padding-top: 28px;
border-top: 1px solid rgba(0,0,0,0.08);
}
.lp-stat-item {}
.lp-stat-num {
font-family: 'Instrument Serif', Georgia, serif;
font-size: 32px;
font-weight: 400;
letter-spacing: -0.03em;
color: #0A0A0A;
line-height: 1;
margin-bottom: 3px;
}
.lp-stat-num .accent { color: #E8334A; }
.lp-stat-label {
font-size: 11px;
font-weight: 500;
color: #A0A0A0;
letter-spacing: 0.02em;
}
/* ─── BOTTOM TICKER ─── */
.lp-ticker {
border-top: 1px solid rgba(0,0,0,0.09);
background: #FFFFFF;
padding: 28px 36px;
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 20px 20px 40px;
justify-content: space-between;
gap: 20px;
animation: lpFadeIn 0.5s 0.35s both;
}
.lp-footer-item {
font-size: 11px;
color: #9BA5B4;
letter-spacing: 0.03em;
.lp-ticker-main {
font-family: 'Instrument Serif', Georgia, serif;
font-size: clamp(26px, 3.5vw, 42px);
font-weight: 400;
letter-spacing: -0.02em;
color: #0A0A0A;
line-height: 1.1;
}
.lp-footer-dot {
width: 3px; height: 3px;
border-radius: 50%;
background: #CBD2DC;
.lp-ticker-main .accent { color: #E8334A; font-style: italic; }
.lp-ticker-main .circle-num {
display: inline-block;
position: relative;
padding: 0 6px;
}
.lp-ticker-main .circle-num::after {
content: '';
position: absolute;
inset: -2px 0;
border: 1.5px solid #0A0A0A;
border-radius: 100px;
}
.lp-ticker-right {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 4px;
flex-shrink: 0;
}
.lp-ticker-badge {
display: inline-flex;
align-items: center;
border: 1px solid rgba(0,0,0,0.14);
border-radius: 100px;
padding: 4px 12px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.06em;
text-transform: uppercase;
color: #5C5C5C;
white-space: nowrap;
}
.lp-ticker-sub {
font-size: 12px;
color: #A0A0A0;
text-align: right;
max-width: 180px;
line-height: 1.5;
}
/* ─── RESPONSIVE ─── */
@media (max-width: 680px) {
.lp-hero { grid-template-columns: 1fr; max-height: none; }
.lp-hero-left { border-right: none; border-bottom: 1px solid rgba(0,0,0,0.07); min-height: auto; }
.lp-hero-right { padding: 32px 24px; }
.lp-nav { padding: 0 16px; }
.lp-ticker { flex-direction: column; align-items: flex-start; padding: 24px; }
.lp-ticker-right { align-items: flex-start; }
}
`;
function LandingPage({ onSelect }) {
const [hoveredCard, setHoveredCard] = useState(null);
return (
<div className="lp-root">
<style>{landingCss}</style>
{/* ── Hero ── */}
{/* ── Hero Split ── */}
<div className="lp-hero">
<div className="lp-eyebrow">
<div className="lp-eyebrow-dot" />
Tugas WebGIS · Pontianak
{/* LEFT: heading + floating cards */}
<div className="lp-hero-left">
<div className="lp-left-eyebrow">
<div className="lp-eyebrow-pip" />
Tugas WebGIS · Pontianak
</div>
<div className="lp-left-heading">
Peta{" "}
<span className="accent">interaktif</span>
<br />
Kota{" "}
<span className="circle-wrap">
Pontianak
<svg viewBox="0 0 1 1" fill="none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="none">
<ellipse cx="0.5" cy="0.5" rx="0.48" ry="0.48" stroke="#0A0A0A" strokeWidth="0.04" fill="none"/>
</svg>
</span>
<br />
berbasis{" "}
<span className="accent">GIS</span>
</div>
{/* Floating project cards */}
<div className="lp-floats">
{projects.map(p => (
<FloatCard key={p.id} project={p} onSelect={onSelect} />
))}
</div>
{/* Coordinate strip */}
<div className="lp-coords-bar">
<span className="lp-coord-chip">0°0155 S</span>
<div className="lp-coord-div" />
<span className="lp-coord-chip">109°2058 E</span>
<div className="lp-coord-div" />
<span className="lp-coord-chip">Kalimantan Barat</span>
</div>
</div>
<h1 className="lp-headline">
Peta Kota<br /><em>Pontianak</em>
</h1>
{/* RIGHT: editorial copy + CTA */}
<div className="lp-hero-right">
<div className="lp-right-tag">Pemetaan Digital</div>
<p className="lp-sub">
Empat proyek pemetaan interaktif berbasis Leaflet.js dari SPBU hingga kepadatan penduduk.
</p>
<div className="lp-right-heading">
Jelajahi data{" "}
<span className="accent">spasial</span>
<br />
Pontianak dengan{" "}
<span className="circle-word">presisi</span>
</div>
<div className="lp-coords">
<span className="lp-coord-item">0°0155S</span>
<div className="lp-coord-sep" />
<span className="lp-coord-item">109°2058E</span>
<div className="lp-coord-sep" />
<span className="lp-coord-item">Kalimantan Barat</span>
<p className="lp-right-body">
Empat proyek WebGIS yang menggabungkan data titik, vektor, dan analisis koropleth langsung di browser, tanpa instalasi.
</p>
<button className="lp-right-cta" onClick={() => onSelect("p01")}>
Jelajahi Proyek
<div className="lp-cta-arrow">
<svg width="10" height="10" viewBox="0 0 12 12" fill="none">
<path d="M2 10L10 2M10 2H4M10 2v6" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
</button>
{/* Stats */}
<div className="lp-right-stats">
<div className="lp-stat-item">
<div className="lp-stat-num"><span className="accent">4</span></div>
<div className="lp-stat-label">Proyek Aktif</div>
</div>
<div className="lp-stat-item">
<div className="lp-stat-num">6</div>
<div className="lp-stat-label">Kecamatan</div>
</div>
<div className="lp-stat-item">
<div className="lp-stat-num">5+</div>
<div className="lp-stat-label">Layer Peta</div>
</div>
</div>
</div>
</div>
{/* ── Cards ── */}
<div className="lp-cards">
{projects.map((p, i) => (
<ProjectCard key={p.id} project={p} index={i} onSelect={onSelect} />
))}
</div>
{/* ── Divider + Footer ── */}
<div className="lp-divider">
<div className="lp-divider-line" />
<span className="lp-divider-text">Leaflet.js · OpenStreetMap · MySQL</span>
<div className="lp-divider-line" />
</div>
<div className="lp-footer">
<span className="lp-footer-item">Leaflet.js</span>
<div className="lp-footer-dot" />
<span className="lp-footer-item">OpenStreetMap</span>
<div className="lp-footer-dot" />
<span className="lp-footer-item">MySQL</span>
<div className="lp-footer-dot" />
<span className="lp-footer-item">React</span>
</div>
</div>
);
}
function ProjectCard({ project: p, index, onSelect }) {
const [hovered, setHovered] = useState(false);
function FloatCard({ project: p, onSelect }) {
const { Icon } = p;
return (
<div
className="lp-card"
className="lp-float-card"
onClick={() => onSelect(p.id)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
borderColor: hovered ? p.accent + "44" : "rgba(0,0,0,0.08)",
}}
>
{/* Icon */}
<div
className="lp-card-icon-wrap"
style={{ background: hovered ? p.accentLight : "rgba(0,0,0,0.04)" }}
className="lp-float-icon"
style={{ background: `linear-gradient(145deg, ${p.gradient[0]}, ${p.gradient[1]})` }}
>
<span style={{ color: hovered ? p.accent : "#5A6474", transition: "color 0.18s" }}>
<Icon />
</span>
<span style={{ color: "#fff" }}><Icon /></span>
</div>
<div className="lp-card-title">{p.title}</div>
<div
className="lp-card-subtitle"
style={{ color: hovered ? p.accent : LP.labelMuted }}
>
{p.subtitle}
<div className="lp-float-tag">{p.tag}</div>
<div className="lp-float-title">{p.title}</div>
<div className="lp-float-desc">{p.subtitle}</div>
<div className="lp-float-arrow">
<svg width="9" height="9" viewBox="0 0 12 12" fill="none">
<path d="M2 10L10 2M10 2H4M10 2v6" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
</svg>
</div>
<div className="lp-card-desc">{p.description}</div>
{/* Arrow */}
<svg className="lp-card-arrow" width="14" height="14" viewBox="0 0 24 24"
fill="none" stroke={p.accent} strokeWidth="2.5">
<path d="M5 12h14M12 5l7 7-7 7"/>
</svg>
</div>
);
}