update frontend
This commit is contained in:
parent
d6e4dba743
commit
f6fe974ecf
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue