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]; return [toast, show];
} }
// ─── Landing Page (redesigned) ──────────────────────────────────────────────── // ─── Landing Page — Editorial "Puls" style ────────────────────────────────────
const LP = { const LP = {
bg: "#F5F6F8", bg: "#F7F6F3",
bgHero: "#FFFFFF",
surface: "#FFFFFF", surface: "#FFFFFF",
surfaceHover: "#F0F4FF", label: "#0A0A0A",
border: "rgba(0,0,0,0.08)", labelSub: "#5C5C5C",
label: "#0F1724", labelMuted: "#A0A0A0",
labelSub: "#5A6474", accent: "#E8334A", // coral-red accent
labelMuted: "#9BA5B4", accentWarm: "#FF6B35", // warm orange for variety
accent: "#2563EB", border: "rgba(0,0,0,0.09)",
accentLight: "rgba(37,99,235,0.08)", g1: ["#FF9500", "#FF6B00"],
accentBorder: "rgba(37,99,235,0.2)", g2: ["#007AFF", "#0040DD"],
amber: "#D97706", g3: ["#E8334A", "#B01E31"],
amberLight: "rgba(217,119,6,0.08)", g4: ["#34C759", "#248A3D"],
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)",
}; };
// SVG icon components for each project // SVG icon components for each project
function IconPin() { function IconPin() {
return ( 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"/> <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"/> <circle cx="12" cy="10" r="3"/>
</svg> </svg>
@@ -463,14 +455,15 @@ function IconPin() {
} }
function IconRoad() { function IconRoad() {
return ( 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="M3 17l3-10 3 4 3-8 3 4 3-10"/> <path d="M3 3l7.07 16.97 2.51-7.39 7.39-2.51L3 3z"/>
<path d="M13 13l6 6"/>
</svg> </svg>
); );
} }
function IconUsers() { function IconUsers() {
return ( 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"/> <path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/>
<circle cx="9" cy="7" r="4"/> <circle cx="9" cy="7" r="4"/>
<path d="M23 21v-2a4 4 0 0 0-3-3.87"/> <path d="M23 21v-2a4 4 0 0 0-3-3.87"/>
@@ -480,7 +473,7 @@ function IconUsers() {
} }
function IconLayers() { function IconLayers() {
return ( 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"/> <polygon points="12 2 2 7 12 12 22 7 12 2"/>
<polyline points="2 17 12 22 22 17"/> <polyline points="2 17 12 22 22 17"/>
<polyline points="2 12 12 17 22 12"/> <polyline points="2 12 12 17 22 12"/>
@@ -495,17 +488,19 @@ const projects = [
title: "SPBU Map", title: "SPBU Map",
subtitle: "Point Layer", subtitle: "Point Layer",
description: "Tambah, hapus, dan geser marker SPBU. Data tersimpan ke MySQL.", description: "Tambah, hapus, dan geser marker SPBU. Data tersimpan ke MySQL.",
accent: LP.amber, gradient: LP.g1,
accentLight: LP.amberLight, gradId: "g1",
tag: "Titik",
}, },
{ {
id: "p02", id: "p02",
Icon: IconRoad, Icon: IconRoad,
title: "WebGIS Jalan & Tanah", title: "Jalan & Tanah",
subtitle: "Vector Edit", subtitle: "Vector Edit",
description: "Gambar dan edit polyline jalan serta polygon parsil tanah. CRUD penuh.", description: "Gambar dan edit polyline jalan serta polygon parsil tanah. CRUD penuh.",
accent: LP.blue, gradient: LP.g2,
accentLight: LP.blueLight, gradId: "g2",
tag: "Vektor",
}, },
{ {
id: "p03", id: "p03",
@@ -513,349 +508,568 @@ const projects = [
title: "Kepadatan Penduduk", title: "Kepadatan Penduduk",
subtitle: "Choropleth", subtitle: "Choropleth",
description: "Koropleth kepadatan per kecamatan dengan tiga kelas warna. Data BPS.", description: "Koropleth kepadatan per kecamatan dengan tiga kelas warna. Data BPS.",
accent: LP.red, gradient: LP.g3,
accentLight: LP.redLight, gradId: "g3",
tag: "Analisis",
}, },
{ {
id: "p04", id: "p04",
Icon: IconLayers, Icon: IconLayers,
title: "SPBU + Layer Toggle", title: "SPBU Layer Toggle",
subtitle: "Layer Control", subtitle: "Layer Control",
description: "Kontrol layer toggle untuk memisahkan SPBU berdasarkan jam operasi.", description: "Kontrol layer toggle untuk memisahkan SPBU berdasarkan jam operasi.",
accent: LP.green, gradient: LP.g4,
accentLight: LP.greenLight, gradId: "g4",
tag: "Layer",
}, },
]; ];
const landingCss = ` 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 { @keyframes lpFadeUp {
from { opacity: 0; transform: translateY(12px); } from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); } 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 { .lp-root {
min-height: 100vh; min-height: 100vh;
background: #F5F6F8; background: #F7F6F3;
font-family: 'DM Sans', -apple-system, sans-serif; font-family: 'Inter', -apple-system, 'Helvetica Neue', sans-serif;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
overflow-x: hidden; 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 { .lp-hero {
background: #FFFFFF; display: grid;
border-bottom: 1px solid rgba(0,0,0,0.06); grid-template-columns: 1fr 1fr;
padding: 72px 24px 60px; min-height: 100vh;
max-height: 900px;
}
/* LEFT PANEL */
.lp-hero-left {
position: relative;
padding: 40px 28px 32px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; border-right: 1px solid rgba(0,0,0,0.07);
text-align: center;
position: relative;
overflow: hidden; overflow: hidden;
animation: lpFadeUp 0.5s 0.05s both;
} }
/* Subtle dot grid — static, no animation */ /* Subtle grid lines — cartographic feel */
.lp-hero::before { .lp-hero-left::before {
content: ''; content: '';
position: absolute; inset: 0; position: absolute; inset: 0;
background-image: radial-gradient(circle, rgba(37,99,235,0.08) 1px, transparent 1px); background-image:
background-size: 28px 28px; linear-gradient(rgba(0,0,0,0.04) 1px, transparent 1px),
pointer-events: none; linear-gradient(90deg, rgba(0,0,0,0.04) 1px, transparent 1px);
} background-size: 40px 40px;
/* 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%);
pointer-events: none; pointer-events: none;
} }
/* All hero content sits above pseudo-elements */ .lp-left-eyebrow {
.lp-hero > * { position: relative; z-index: 1; }
/* ── eyebrow ── */
.lp-eyebrow {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 7px; gap: 7px;
background: rgba(37,99,235,0.06); background: #FFFFFF;
border: 1px solid rgba(37,99,235,0.18); border: 1px solid rgba(0,0,0,0.10);
border-radius: 100px; border-radius: 100px;
padding: 5px 14px; padding: 5px 12px 5px 8px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
letter-spacing: 0.11em; letter-spacing: 0.06em;
text-transform: uppercase; text-transform: uppercase;
color: #2563EB; color: #5C5C5C;
margin-bottom: 22px; width: fit-content;
animation: lpFadeUp 0.45s 0.05s both; margin-bottom: 28px;
position: relative; z-index: 1;
} }
.lp-eyebrow-dot { .lp-eyebrow-pip {
width: 6px; height: 6px; width: 6px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: #2563EB; background: #E8334A;
flex-shrink: 0;
} }
/* ── headline ── */ .lp-left-heading {
.lp-headline { font-family: 'Instrument Serif', Georgia, serif;
font-family: 'Playfair Display', Georgia, serif; font-size: clamp(38px, 5.5vw, 58px);
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;
font-weight: 400; font-weight: 400;
color: #5A6474; line-height: 1.06;
max-width: 360px; letter-spacing: -0.02em;
line-height: 1.65; color: #0A0A0A;
margin-bottom: 28px; margin-bottom: 0;
animation: lpFadeUp 0.45s 0.18s both; position: relative; z-index: 1;
flex: 1;
} }
.lp-left-heading .accent {
/* ── coordinates strip ── */ color: #E8334A;
.lp-coords { font-style: italic;
display: flex;
gap: 14px;
align-items: center;
animation: lpFadeUp 0.45s 0.24s both;
} }
.lp-coord-item { .lp-left-heading .circle-wrap {
font-size: 11px; display: inline-block;
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 {
position: relative; 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; border-radius: 14px;
padding: 20px 18px 42px; padding: 14px;
cursor: pointer; cursor: pointer;
transition: border-color 0.18s, transform 0.18s, box-shadow 0.18s, background 0.18s; transition: transform 0.18s cubic-bezier(0.34,1.56,0.64,1), box-shadow 0.18s;
text-align: left; position: relative;
overflow: hidden; overflow: hidden;
} }
.lp-card:hover { .lp-float-card:hover {
transform: translateY(-2px); transform: translateY(-3px) scale(1.015);
box-shadow: 0 8px 28px rgba(0,0,0,0.08); box-shadow: 0 12px 32px rgba(0,0,0,0.10);
} }
.lp-float-card:active { transform: scale(0.97); }
.lp-card-icon-wrap { .lp-float-arrow {
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 {
position: absolute; position: absolute;
bottom: 16px; right: 16px; top: 10px; right: 10px;
opacity: 0; width: 22px; height: 22px;
transition: opacity 0.18s, transform 0.18s; border-radius: 50%;
transform: translateX(-6px); background: #0A0A0A;
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
} }
.lp-card:hover .lp-card-arrow { .lp-float-tag {
opacity: 1; font-size: 9px;
transform: translateX(0); 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 ── */ /* Coords bar bottom-left */
.lp-divider { .lp-coords-bar {
width: 100%;
max-width: 660px;
margin: 28px auto 0;
padding: 0 20px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 14px; gap: 12px;
margin-top: 20px;
position: relative; z-index: 1;
} }
.lp-divider-line { .lp-coord-chip {
flex: 1; height: 1px;
background: rgba(0,0,0,0.07);
}
.lp-divider-text {
font-size: 10px; font-size: 10px;
font-weight: 500; font-weight: 500;
letter-spacing: 0.08em; letter-spacing: 0.05em;
text-transform: uppercase; color: #A0A0A0;
color: #9BA5B4; font-variant-numeric: tabular-nums;
white-space: nowrap; }
.lp-coord-div {
width: 1px; height: 8px;
background: rgba(0,0,0,0.15);
} }
/* ── footer ── */ /* RIGHT PANEL */
.lp-footer { .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; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: space-between;
gap: 10px; gap: 20px;
padding: 20px 20px 40px; animation: lpFadeIn 0.5s 0.35s both;
} }
.lp-footer-item { .lp-ticker-main {
font-size: 11px; font-family: 'Instrument Serif', Georgia, serif;
color: #9BA5B4; font-size: clamp(26px, 3.5vw, 42px);
letter-spacing: 0.03em; font-weight: 400;
letter-spacing: -0.02em;
color: #0A0A0A;
line-height: 1.1;
} }
.lp-footer-dot { .lp-ticker-main .accent { color: #E8334A; font-style: italic; }
width: 3px; height: 3px; .lp-ticker-main .circle-num {
border-radius: 50%; display: inline-block;
background: #CBD2DC; 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; 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 }) { function LandingPage({ onSelect }) {
const [hoveredCard, setHoveredCard] = useState(null);
return ( return (
<div className="lp-root"> <div className="lp-root">
<style>{landingCss}</style> <style>{landingCss}</style>
{/* ── Hero ── */} {/* ── Hero Split ── */}
<div className="lp-hero"> <div className="lp-hero">
<div className="lp-eyebrow">
<div className="lp-eyebrow-dot" /> {/* LEFT: heading + floating cards */}
Tugas WebGIS · Pontianak <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> </div>
<h1 className="lp-headline"> {/* RIGHT: editorial copy + CTA */}
Peta Kota<br /><em>Pontianak</em> <div className="lp-hero-right">
</h1> <div className="lp-right-tag">Pemetaan Digital</div>
<p className="lp-sub"> <div className="lp-right-heading">
Empat proyek pemetaan interaktif berbasis Leaflet.js dari SPBU hingga kepadatan penduduk. Jelajahi data{" "}
</p> <span className="accent">spasial</span>
<br />
Pontianak dengan{" "}
<span className="circle-word">presisi</span>
</div>
<div className="lp-coords"> <p className="lp-right-body">
<span className="lp-coord-item">0°0155S</span> Empat proyek WebGIS yang menggabungkan data titik, vektor, dan analisis koropleth langsung di browser, tanpa instalasi.
<div className="lp-coord-sep" /> </p>
<span className="lp-coord-item">109°2058E</span>
<div className="lp-coord-sep" /> <button className="lp-right-cta" onClick={() => onSelect("p01")}>
<span className="lp-coord-item">Kalimantan Barat</span> 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>
</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> </div>
); );
} }
function ProjectCard({ project: p, index, onSelect }) { function FloatCard({ project: p, onSelect }) {
const [hovered, setHovered] = useState(false);
const { Icon } = p; const { Icon } = p;
return ( return (
<div <div
className="lp-card" className="lp-float-card"
onClick={() => onSelect(p.id)} onClick={() => onSelect(p.id)}
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
borderColor: hovered ? p.accent + "44" : "rgba(0,0,0,0.08)",
}}
> >
{/* Icon */}
<div <div
className="lp-card-icon-wrap" className="lp-float-icon"
style={{ background: hovered ? p.accentLight : "rgba(0,0,0,0.04)" }} style={{ background: `linear-gradient(145deg, ${p.gradient[0]}, ${p.gradient[1]})` }}
> >
<span style={{ color: hovered ? p.accent : "#5A6474", transition: "color 0.18s" }}> <span style={{ color: "#fff" }}><Icon /></span>
<Icon />
</span>
</div> </div>
<div className="lp-float-tag">{p.tag}</div>
<div className="lp-card-title">{p.title}</div> <div className="lp-float-title">{p.title}</div>
<div <div className="lp-float-desc">{p.subtitle}</div>
className="lp-card-subtitle" <div className="lp-float-arrow">
style={{ color: hovered ? p.accent : LP.labelMuted }} <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"/>
{p.subtitle} </svg>
</div> </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> </div>
); );
} }