update frontend

This commit is contained in:
flems 2026-05-21 11:01:50 +07:00
parent d6e4dba743
commit f6fe974ecf
3 changed files with 238 additions and 48 deletions

View File

@ -19,10 +19,14 @@ Web javaherbal sekarang sudah **responsive** dan tidak terpotong di HP!
### 2. **Navbar.tsx**
- ✅ Responsive height: 64px → 56px (mobile)
- ✅ Responsive padding: 2rem → 1rem (mobile)
- ✅ Hide navigation links di mobile
- ✅ Hide user name di mobile
- ✅ Smaller buttons dan icons di mobile
- ✅ Hide "Keluar" text di mobile (icon only)
- ✅ **Hamburger menu di mobile** (icon Menu/X dari lucide-react)
- ✅ Mobile menu dropdown dengan semua navigation links
- ✅ Show user info di mobile menu
- ✅ Full-width login/logout button di mobile menu
- ✅ Auto-close menu saat route berubah
- ✅ Auto-close menu saat resize ke desktop
- ✅ Smooth transitions dan hover effects
- ✅ Active link highlighting di mobile menu
### 3. **Footer.tsx**
- ✅ Grid responsive: 4 kolom → 1 kolom (mobile)
@ -73,11 +77,12 @@ Web javaherbal sekarang sudah **responsive** dan tidak terpotong di HP!
### Mobile (≤ 768px)
- Navbar: 56px height
- **Hamburger menu icon** (Menu/X toggle)
- **Mobile dropdown menu** dengan semua navigation links
- Grid: 1 kolom
- Padding: 3rem 1rem
- Font size: Smaller
- Input font: 16px (prevent zoom)
- Hide navigation links
- Compact buttons
---
@ -119,12 +124,13 @@ style={{
### Mobile
1. Buka web di HP
2. **Expected:** Tidak ada horizontal scroll
3. **Expected:** UI tidak terpotong
4. **Expected:** Text readable
5. **Expected:** Input tidak zoom saat focus
6. **Expected:** Navbar compact dengan icon only
7. **Expected:** Footer stacked vertical
2. **Expected:** Hamburger menu icon (☰) muncul di kanan atas
3. **Expected:** Klik hamburger, dropdown menu muncul dengan semua links
4. **Expected:** Klik link, menu auto-close dan navigate
5. **Expected:** Tidak ada horizontal scroll
6. **Expected:** UI tidak terpotong
7. **Expected:** Text readable
8. **Expected:** Input tidak zoom saat focus
---
@ -132,6 +138,9 @@ style={{
- [x] Prevent horizontal scroll
- [x] Responsive navbar (hide links, compact buttons)
- [x] **Hamburger menu untuk mobile navigation**
- [x] Mobile menu dropdown dengan semua links
- [x] Auto-close menu on route change
- [x] Responsive footer (4 col → 1 col)
- [x] Responsive grid (2-4 col → 1 col)
- [x] Responsive padding (all pages)
@ -153,8 +162,25 @@ Semua halaman sudah disesuaikan:
- ✅ Home (Hero, Dashboard)
- ✅ Products
- ✅ About
- ✅ Control Panel
- ✅ Control Panel (gauge tidak terpotong)
- ✅ Login
- ✅ Register
- ✅ Navbar
- ✅ **Navbar dengan Hamburger Menu** 🍔
- ✅ Footer
## 🍔 Fitur Hamburger Menu
Di mobile (≤ 768px):
1. **Icon hamburger** (☰) muncul di kanan atas navbar
2. **Klik icon** → Dropdown menu muncul dari atas
3. **Menu berisi:**
- User info (jika login)
- Semua navigation links (Beranda, Produk, Tentang Kami)
- Control Panel link (jika login)
- Button Login/Logout
4. **Auto-close** saat:
- Klik link (navigate ke halaman lain)
- Resize window ke desktop
- Klik icon X
5. **Smooth animations** dan hover effects
6. **Active link highlighting** (warna hijau untuk halaman aktif)

View File

@ -23,9 +23,9 @@ interface SensorData {
function Gauge({ value, max }: { value: number; max: number }) {
const cx = 90
const cy = 95
const r = 68
const cx = 100
const cy = 100
const r = 70
const startDeg = -210
const endDeg = 30
const totalSweep = endDeg - startDeg
@ -45,7 +45,7 @@ function Gauge({ value, max }: { value: number; max: number }) {
const fillAngle = startDeg + (value / max) * totalSweep
return (
<svg width="180" height="155" viewBox="0 0 180 155">
<svg width="200" height="170" viewBox="0 0 200 170" style={{ overflow: 'visible' }}>
<defs>
<linearGradient id="gGrad" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" stopColor="#4a7c2f" />
@ -53,12 +53,12 @@ function Gauge({ value, max }: { value: number; max: number }) {
</linearGradient>
</defs>
{/* Track */}
<path d={arc(startDeg, endDeg)} fill="none" stroke="#e8eee0" strokeWidth="11" strokeLinecap="round" />
<path d={arc(startDeg, endDeg)} fill="none" stroke="#e8eee0" strokeWidth="12" strokeLinecap="round" />
{/* Fill */}
<path d={arc(startDeg, fillAngle)} fill="none" stroke="url(#gGrad)" strokeWidth="11" strokeLinecap="round" />
<text x={cx} y={cy - 22} textAnchor="middle" fill="#7ab648" fontSize="9" fontWeight="700" fontFamily="Plus Jakarta Sans, sans-serif">SUHU</text>
<text x={cx} y={cy + 3} textAnchor="middle" fill="#8a9a7a" fontSize="10" fontFamily="Plus Jakarta Sans, sans-serif">SAAT INI:</text>
<text x={cx} y={cy + 28} textAnchor="middle" fill="#1a2810" fontSize="26" fontWeight="800" fontFamily="Plus Jakarta Sans, sans-serif">{value.toFixed(1)}°C</text>
<path d={arc(startDeg, fillAngle)} fill="none" stroke="url(#gGrad)" strokeWidth="12" strokeLinecap="round" />
<text x={cx} y={cy - 22} textAnchor="middle" fill="#7ab648" fontSize="10" fontWeight="700" fontFamily="Plus Jakarta Sans, sans-serif">SUHU</text>
<text x={cx} y={cy + 3} textAnchor="middle" fill="#8a9a7a" fontSize="11" fontFamily="Plus Jakarta Sans, sans-serif">SAAT INI:</text>
<text x={cx} y={cy + 30} textAnchor="middle" fill="#1a2810" fontSize="28" fontWeight="800" fontFamily="Plus Jakarta Sans, sans-serif">{value.toFixed(1)}°C</text>
</svg>
)
}
@ -334,7 +334,13 @@ export default function ControlPanel() {
</div>
) : (
<>
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '1.2rem' }}>
<div style={{
display: 'flex',
justifyContent: 'center',
marginBottom: '1.2rem',
padding: '0.5rem',
overflow: 'visible'
}}>
<Gauge value={currentTemp} max={100} />
</div>

View File

@ -1,6 +1,6 @@
import React, { useState } from 'react'
import { Link, useLocation, useNavigate } from 'react-router-dom'
import { Leaf, LogOut } from 'lucide-react'
import { Leaf, LogOut, Menu, X } from 'lucide-react'
const NAV_LINKS = [
{ label: 'Beranda', path: '/' },
@ -37,23 +37,38 @@ function NavLink({ label, path }: { label: string; path: string }) {
export default function Navbar() {
const navigate = useNavigate()
const location = useLocation()
const isAuthenticated = localStorage.getItem('isAuthenticated') === 'true'
const currentUser = JSON.parse(localStorage.getItem('currentUser') || '{}')
const [isMobile, setIsMobile] = useState(window.innerWidth <= 768)
const [menuOpen, setMenuOpen] = useState(false)
React.useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth <= 768)
const handleResize = () => {
setIsMobile(window.innerWidth <= 768)
if (window.innerWidth > 768) {
setMenuOpen(false) // Close menu when resizing to desktop
}
}
window.addEventListener('resize', handleResize)
return () => window.removeEventListener('resize', handleResize)
}, [])
// Close menu when route changes
React.useEffect(() => {
setMenuOpen(false)
}, [location.pathname])
function handleLogout() {
localStorage.removeItem('isAuthenticated')
localStorage.removeItem('currentUser')
localStorage.removeItem('token')
setMenuOpen(false)
navigate('/')
}
function handleLogin() {
setMenuOpen(false)
navigate('/login')
}
@ -98,7 +113,7 @@ export default function Navbar() {
</span>
</Link>
{/* Links - Hide on mobile */}
{/* Links - Desktop only */}
{!isMobile && (
<div style={{ display: 'flex', gap: '2rem' }}>
{NAV_LINKS.map((link) => (
@ -112,21 +127,23 @@ export default function Navbar() {
{/* Actions */}
<div style={{ display: 'flex', gap: isMobile ? '0.5rem' : '0.8rem', alignItems: 'center' }}>
{isAuthenticated ? (
<>
{!isMobile && currentUser.name && (
<span style={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.85rem', fontWeight: 500 }}>
{currentUser.name}
</span>
)}
{/* Desktop: Show user name and logout */}
{!isMobile && isAuthenticated && currentUser.name && (
<span style={{ color: 'rgba(255,255,255,0.85)', fontSize: '0.85rem', fontWeight: 500 }}>
{currentUser.name}
</span>
)}
{!isMobile && (
isAuthenticated ? (
<button
onClick={handleLogout}
style={{
background: 'linear-gradient(135deg,#7ab648,#4a7c2f)',
borderRadius: '8px',
padding: isMobile ? '6px 12px' : '8px 18px',
padding: '8px 18px',
color: 'white',
fontSize: isMobile ? '0.75rem' : '0.85rem',
fontSize: '0.85rem',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
@ -135,28 +152,169 @@ export default function Navbar() {
border: 'none',
}}
>
<LogOut size={isMobile ? 14 : 16} />
{!isMobile && 'Keluar'}
<LogOut size={16} />
Keluar
</button>
</>
) : (
) : (
<button
onClick={handleLogin}
style={{
background: 'linear-gradient(135deg,#7ab648,#4a7c2f)',
borderRadius: '8px',
padding: '8px 18px',
color: 'white',
fontSize: '0.85rem',
fontWeight: 600,
cursor: 'pointer',
border: 'none',
}}
>
Masuk
</button>
)
)}
{/* Mobile: Hamburger menu */}
{isMobile && (
<button
onClick={handleLogin}
onClick={() => setMenuOpen(!menuOpen)}
style={{
background: 'linear-gradient(135deg,#7ab648,#4a7c2f)',
borderRadius: '8px',
padding: isMobile ? '6px 12px' : '8px 18px',
color: 'white',
fontSize: isMobile ? '0.75rem' : '0.85rem',
fontWeight: 600,
cursor: 'pointer',
background: 'transparent',
border: 'none',
color: 'white',
cursor: 'pointer',
padding: '8px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
}}
>
Masuk
{menuOpen ? <X size={24} /> : <Menu size={24} />}
</button>
)}
</div>
{/* Mobile Menu Dropdown */}
{isMobile && menuOpen && (
<div
style={{
position: 'fixed',
top: '56px',
left: 0,
right: 0,
background: 'rgba(27,46,15,0.98)',
backdropFilter: 'blur(12px)',
boxShadow: '0 4px 24px rgba(0,0,0,0.2)',
zIndex: 99,
padding: '1rem',
maxHeight: 'calc(100vh - 56px)',
overflowY: 'auto',
}}
>
{/* User info if logged in */}
{isAuthenticated && currentUser.name && (
<div
style={{
padding: '1rem',
borderBottom: '1px solid rgba(255,255,255,0.1)',
marginBottom: '1rem',
}}
>
<span style={{ color: 'rgba(255,255,255,0.6)', fontSize: '0.75rem', display: 'block', marginBottom: '0.25rem' }}>
Masuk sebagai
</span>
<span style={{ color: 'white', fontSize: '0.9rem', fontWeight: 600 }}>
{currentUser.name}
</span>
</div>
)}
{/* Navigation Links */}
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.5rem' }}>
{NAV_LINKS.map((link) => (
<Link
key={link.path}
to={link.path}
style={{
color: location.pathname === link.path ? '#7ab648' : 'rgba(255,255,255,0.85)',
textDecoration: 'none',
fontSize: '0.95rem',
fontWeight: location.pathname === link.path ? 600 : 500,
padding: '0.75rem 1rem',
borderRadius: '8px',
background: location.pathname === link.path ? 'rgba(122,182,72,0.15)' : 'transparent',
transition: 'all 0.2s',
}}
>
{link.label}
</Link>
))}
{isAuthenticated && PROTECTED_LINKS.map((link) => (
<Link
key={link.path}
to={link.path}
style={{
color: location.pathname === link.path ? '#7ab648' : 'rgba(255,255,255,0.85)',
textDecoration: 'none',
fontSize: '0.95rem',
fontWeight: location.pathname === link.path ? 600 : 500,
padding: '0.75rem 1rem',
borderRadius: '8px',
background: location.pathname === link.path ? 'rgba(122,182,72,0.15)' : 'transparent',
transition: 'all 0.2s',
}}
>
{link.label}
</Link>
))}
</div>
{/* Login/Logout Button */}
<div style={{ marginTop: '1rem', paddingTop: '1rem', borderTop: '1px solid rgba(255,255,255,0.1)' }}>
{isAuthenticated ? (
<button
onClick={handleLogout}
style={{
width: '100%',
background: 'linear-gradient(135deg,#7ab648,#4a7c2f)',
borderRadius: '8px',
padding: '0.75rem',
color: 'white',
fontSize: '0.9rem',
fontWeight: 600,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
cursor: 'pointer',
border: 'none',
}}
>
<LogOut size={18} />
Keluar
</button>
) : (
<button
onClick={handleLogin}
style={{
width: '100%',
background: 'linear-gradient(135deg,#7ab648,#4a7c2f)',
borderRadius: '8px',
padding: '0.75rem',
color: 'white',
fontSize: '0.9rem',
fontWeight: 600,
cursor: 'pointer',
border: 'none',
}}
>
Masuk
</button>
)}
</div>
</div>
)}
</nav>
)
}