245 lines
9.7 KiB
TypeScript
245 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import React, { useState } from 'react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { User, Lock, ArrowRight, Eye, EyeOff, Activity, ShieldCheck, Cloud } from 'lucide-react';
|
|
import styles from './Login.module.css';
|
|
|
|
export default function Login() {
|
|
const router = useRouter();
|
|
const [username, setUsername] = useState('');
|
|
const [password, setPassword] = useState('');
|
|
const [showPassword, setShowPassword] = useState(false);
|
|
const [rememberMe, setRememberMe] = useState(false);
|
|
const [error, setError] = useState(false);
|
|
const [loading, setLoading] = useState(false);
|
|
|
|
const handleSubmit = async (e: React.FormEvent) => {
|
|
e.preventDefault();
|
|
if (username.trim() === '' || password.trim() === '') return;
|
|
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch('/api/auth/login', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ username, password }),
|
|
});
|
|
|
|
if (res.ok) {
|
|
router.push('/');
|
|
router.refresh();
|
|
} else {
|
|
setError(true);
|
|
setTimeout(() => setError(false), 2000);
|
|
setPassword('');
|
|
}
|
|
} catch (err) {
|
|
setError(true);
|
|
setTimeout(() => setError(false), 2000);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={styles.container}>
|
|
{/* Kolom Kiri: Form Login Bahasa Indonesia Rapi */}
|
|
<div className={styles.leftColumn}>
|
|
<div /> {/* Spacer vertical alignments */}
|
|
|
|
<div className={styles.formContainer}>
|
|
<div className={styles.header}>
|
|
<h1 className={styles.title}>Selamat Datang</h1>
|
|
<p className={styles.subtitle}>
|
|
Masuk untuk memantau dan mengelola sistem UPS DC berbasis IoT Anda.
|
|
</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit} className={styles.form}>
|
|
<div className={styles.fieldGroup}>
|
|
<label className={styles.label} htmlFor="username">Username</label>
|
|
<div className={styles.inputWrapper}>
|
|
<div className={styles.inputPrefix}>
|
|
<User size={18} />
|
|
</div>
|
|
<input
|
|
id="username"
|
|
type="text"
|
|
placeholder="Masukkan username"
|
|
className={`${styles.input} ${error ? styles.inputError : ''}`}
|
|
value={username}
|
|
onChange={(e) => {
|
|
setUsername(e.target.value);
|
|
setError(false);
|
|
}}
|
|
autoFocus
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.fieldGroup}>
|
|
<label className={styles.label} htmlFor="password">Password</label>
|
|
<div className={styles.inputWrapper}>
|
|
<div className={styles.inputPrefix}>
|
|
<Lock size={18} />
|
|
</div>
|
|
<input
|
|
id="password"
|
|
type={showPassword ? 'text' : 'password'}
|
|
placeholder="Masukkan password"
|
|
className={`${styles.input} ${error ? styles.inputError : ''}`}
|
|
value={password}
|
|
onChange={(e) => {
|
|
setPassword(e.target.value);
|
|
setError(false);
|
|
}}
|
|
/>
|
|
<button
|
|
type="button"
|
|
className={styles.inputSuffix}
|
|
onClick={() => setShowPassword(!showPassword)}
|
|
tabIndex={-1}
|
|
aria-label="Toggle password visibility"
|
|
>
|
|
{showPassword ? <EyeOff size={18} /> : <Eye size={18} />}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{error && <span className={styles.errorMessage}>Username atau password salah</span>}
|
|
|
|
{/* Opsi Ingat Saya */}
|
|
<div className={styles.optionsRow}>
|
|
<label className={styles.rememberMe}>
|
|
<input
|
|
type="checkbox"
|
|
className={styles.checkbox}
|
|
checked={rememberMe}
|
|
onChange={(e) => setRememberMe(e.target.checked)}
|
|
/>
|
|
<span>Ingat saya</span>
|
|
</label>
|
|
</div>
|
|
|
|
{/* Tombol Masuk Solid (Tanpa Glow) */}
|
|
<button type="submit" className={styles.submitBtn} disabled={loading}>
|
|
<span>{loading ? 'Masuk...' : 'Masuk'}</span>
|
|
{!loading && <ArrowRight size={18} />}
|
|
</button>
|
|
</form>
|
|
</div>
|
|
|
|
<div /> {/* Bottom spacer for vertical alignment */}
|
|
</div>
|
|
|
|
{/* Kolom Kanan: Nuansa Biru Muda Lembut dengan Fitur Bahasa Indonesia */}
|
|
<div className={styles.rightColumn}>
|
|
<div className={styles.showcaseContainer}>
|
|
{/* Bagian Hero + Ilustrasi Tameng Konsentris di Kanan */}
|
|
<div className={styles.heroRow}>
|
|
<div className={styles.heroText}>
|
|
<span className={styles.subTag}>Sistem UPS DC</span>
|
|
<h2 className={styles.showcaseHeading}>
|
|
Monitoring <span>Cerdas.</span><br />
|
|
Perlindungan <span>Optimal.</span>
|
|
</h2>
|
|
<div className={styles.titleDivider} />
|
|
<p className={styles.showcaseDesc}>
|
|
Solusi pemantauan dan perlindungan daya DC secara real-time untuk memastikan sistem Anda selalu andal.
|
|
</p>
|
|
</div>
|
|
|
|
{/* Ilustrasi Lingkaran Konsentris Tameng Petir (Sesuai Referensi) */}
|
|
<div className={styles.shieldIllustration}>
|
|
<svg width="260" height="260" viewBox="0 0 280 280" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
{/* Lingkaran Luar Putus-Putus & Titik Orbit (Posisi Segitiga Emas: 11:00, 04:00, 07:00) */}
|
|
<g className={styles.orbitRing}>
|
|
<circle cx="140" cy="140" r="130" stroke="#bfdbfe" strokeWidth="1.5" strokeDasharray="5 8" />
|
|
<circle cx="75" cy="28" r="5" fill="#2563eb" /> {/* Pukul 11:00 */}
|
|
<circle cx="253" cy="205" r="5" fill="#3b82f6" /> {/* Pukul 04:00 */}
|
|
<circle cx="75" cy="252" r="4.5" fill="#60a5fa" /> {/* Pukul 07:00 */}
|
|
</g>
|
|
|
|
{/* Lingkaran Menengah & Dalam (Berdenyut Aktif/Pulse) */}
|
|
<circle cx="140" cy="140" r="100" stroke="#dbeafe" strokeWidth="1.5" />
|
|
<g className={styles.pulseGlow}>
|
|
<circle cx="140" cy="140" r="75" fill="#60a5fa" fillOpacity="0.18" stroke="#93c5fd" strokeWidth="2" />
|
|
</g>
|
|
<circle cx="140" cy="140" r="55" fill="#ffffff" fillOpacity="0.95" />
|
|
<circle cx="140" cy="140" r="55" stroke="#eff6ff" strokeWidth="6" />
|
|
|
|
{/* Ikon Shield & Petir di Tengah (Melayang Elegan) */}
|
|
<g className={styles.floatingShield}>
|
|
<path
|
|
d="M 140 105 L 175 118 C 175 145 162 168 140 177 C 118 168 105 145 105 118 L 140 105 Z"
|
|
stroke="#2563eb"
|
|
strokeWidth="3.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
fill="#eff6ff"
|
|
/>
|
|
<path
|
|
d="M 143 122 L 132 142 L 146 142 L 137 162 L 152 138 L 139 138 L 143 122 Z"
|
|
fill="#2563eb"
|
|
/>
|
|
</g>
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Daftar 3 Poin Keunggulan (Boks Ikon Biru Muda Lembut #dbeafe) */}
|
|
<div className={styles.featureList}>
|
|
<div className={styles.featureItem}>
|
|
<div className={styles.featureIconBox}>
|
|
<Activity size={26} strokeWidth={2.2} />
|
|
</div>
|
|
<div className={styles.featureText}>
|
|
<span className={styles.featureTitle}>Pemantauan Real-time</span>
|
|
<span className={styles.featureDesc}>
|
|
Pantau tegangan, arus, dan status daya secara real-time dari mana saja.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.featureItem}>
|
|
<div className={styles.featureIconBox}>
|
|
<ShieldCheck size={26} strokeWidth={2.2} />
|
|
</div>
|
|
<div className={styles.featureText}>
|
|
<span className={styles.featureTitle}>Perlindungan Cerdas</span>
|
|
<span className={styles.featureDesc}>
|
|
Sistem proteksi otomatis terhadap over-voltage, over-discharge, dan kondisi abnormal lainnya.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div className={styles.featureItem}>
|
|
<div className={styles.featureIconBox}>
|
|
<Cloud size={26} strokeWidth={2.2} />
|
|
</div>
|
|
<div className={styles.featureText}>
|
|
<span className={styles.featureTitle}>Akses Cloud IoT</span>
|
|
<span className={styles.featureDesc}>
|
|
Data tersimpan aman di cloud dan dapat diakses kapan saja untuk analisis dan histori.
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Dekorasi Dot Grid di Pojok Kiri/Kanan Bawah */}
|
|
<div className={styles.bgDots}>
|
|
<svg width="120" height="90" viewBox="0 0 120 90" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
{Array.from({ length: 5 }).map((_, r) =>
|
|
Array.from({ length: 7 }).map((_, c) => (
|
|
<circle key={`${r}-${c}`} cx={10 + c * 16} cy={10 + r * 16} r="2" fill="#93c5fd" />
|
|
))
|
|
)}
|
|
</svg>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|