From a501b4845e97f279167ba5f9ca405c49fe5d789f Mon Sep 17 00:00:00 2001 From: Evelyn Sucitro Date: Fri, 14 Nov 2025 23:23:25 +0700 Subject: [PATCH] Frontend --- Animation Login.js | 81 ++++++++++++++++++++++++++++++++++++ Login.css | 22 +++++++++- Login.html | 85 +++++++++++++++++++++----------------- Login.js | 101 +++++++++++++++++++++++++++++++++++++++++++++ Modal.js | 50 ++++++++++++++++++++++ 5 files changed, 298 insertions(+), 41 deletions(-) create mode 100644 Modal.js diff --git a/Animation Login.js b/Animation Login.js index e69de29..9a24f9f 100644 --- a/Animation Login.js +++ b/Animation Login.js @@ -0,0 +1,81 @@ +// Particle System +const particlesContainer = document.getElementById('particles'); +const particleCount = 150; +const particles = []; + +class Particle { + constructor() { + this.element = document.createElement('div'); + this.element.className = 'particle'; + this.reset(); + particlesContainer.appendChild(this.element); + } + + reset() { + this.x = Math.random() * window.innerWidth; + this.y = Math.random() * window.innerHeight; + this.vx = (Math.random() - 0.5) * 1.2; + this.vy = (Math.random() - 0.5) * 1.2; + this.size = Math.random() * 3 + 2; + + const colors = [ + '#00d9ff', '#ff00ff', '#00ffff', + '#ff0080', '#9d00ff', '#00ff88' + ]; + + const color = colors[Math.floor(Math.random() * colors.length)]; + this.element.style.background = color; + this.element.style.boxShadow = `0 0 15px ${color}`; + this.element.style.width = `${this.size}px`; + this.element.style.height = `${this.size}px`; + + this.updatePosition(); + } + + updatePosition() { + this.element.style.left = `${this.x}px`; + this.element.style.top = `${this.y}px`; + } + + move() { + this.x += this.vx; + this.y += this.vy; + + if (this.x < -10) this.x = window.innerWidth + 10; + if (this.x > window.innerWidth + 10) this.x = -10; + if (this.y < -10) this.y = window.innerHeight + 10; + if (this.y > window.innerHeight + 10) this.y = -10; + + this.updatePosition(); + } +} + +// Create particles with even distribution +const cols = 15; +const rows = 10; +let particleIndex = 0; + +for (let i = 0; i < rows; i++) { + for (let j = 0; j < cols; j++) { + if (particleIndex >= particleCount) break; + + const particle = new Particle(); + + // Even distribution + slight random offset + particle.x = (j / cols) * window.innerWidth + (Math.random() - 0.5) * 100; + particle.y = (i / rows) * window.innerHeight + (Math.random() - 0.5) * 100; + particle.updatePosition(); + + particles.push(particle); + particleIndex++; + } + if (particleIndex >= particleCount) break; +} + +// Animate +function animate() { + particles.forEach(p => p.move()); + requestAnimationFrame(animate); +} + +animate(); \ No newline at end of file diff --git a/Login.css b/Login.css index af3fa19..c8f6e48 100644 --- a/Login.css +++ b/Login.css @@ -37,12 +37,30 @@ body { z-index: 2; background: rgba(30, 0, 50, 0.6); backdrop-filter: blur(10px); - border: 1px solid rgba(255, 255, 255, 0.1); + border: 2px solid rgba(0, 217, 255, 0.3); border-radius: 20px; padding: 50px 40px; width: 90%; max-width: 400px; - box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(0, 217, 255, 0.3), + inset 0 0 30px rgba(0, 217, 255, 0.1); + animation: glowBorder 3s ease-in-out infinite; +} + +@keyframes glowBorder { + 0%, 100% { + border-color: rgba(0, 217, 255, 0.4); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 40px rgba(0, 217, 255, 0.3), + inset 0 0 30px rgba(0, 217, 255, 0.1); + } + 50% { + border-color: rgba(255, 0, 255, 0.5); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.5), + 0 0 50px rgba(255, 0, 255, 0.4), + inset 0 0 40px rgba(255, 0, 255, 0.15); + } } h1 { diff --git a/Login.html b/Login.html index bfd0253..dd9f08d 100644 --- a/Login.html +++ b/Login.html @@ -1,45 +1,52 @@ - - - - Login - - - -
- -
-

Login

- -
-
- -
- -
- -
- - -
- - + + + + Login + + + +
+ +
+

Login

+ +
+
+
- -
+ + + + + + + + + diff --git a/Login.js b/Login.js index e69de29..ab92639 100644 --- a/Login.js +++ b/Login.js @@ -0,0 +1,101 @@ +import { showModal, closeModal } from "./Modal.js"; +import { loginRequest } from "./API Login.js"; + +document.getElementById('loginForm').addEventListener('submit', async function(e) { + e.preventDefault(); + + const username = document.getElementById('username').value.trim(); + const password = document.getElementById('password').value.trim(); + + // Validasi input kosong + if (!username || !password) { + showModal('error', 'Login Gagal!', 'Username dan password tidak boleh kosong.'); + return; + } + + // Loading state + const submitBtn = this.querySelector('button[type="submit"]'); + const originalText = submitBtn.textContent; + submitBtn.innerHTML = 'Memproses...'; + submitBtn.disabled = true; + + try { + const data = await loginRequest(username, password); + + if (data.success) { + // Simpan data user ke localStorage + localStorage.setItem('authToken', data.token); + localStorage.setItem('username', data.username); + + // Tampilkan modal sukses + showModal('success', 'Login Berhasil!', `Selamat datang, ${data.username}!`); + + } else { + // Handle error dari backend + showModal('error', 'Login Gagal!', data.message || 'Username atau password salah.'); + } + } catch (error) { + console.error('Login Error:', error); + + // Handle berbagai jenis error + let errorMessage = 'Terjadi kesalahan koneksi ke server.'; + + if (error.message === 'Failed to fetch') { + errorMessage = 'Tidak dapat terhubung ke server. Periksa koneksi internet Anda.'; + } else if (error.message.includes('timeout')) { + errorMessage = 'Request timeout. Server tidak merespons.'; + } else if (error.response) { + // Error dari server dengan response + errorMessage = error.response.data?.message || 'Terjadi kesalahan pada server.'; + } else if (error.status === 401) { + errorMessage = 'Username atau password salah.'; + } else if (error.status === 403) { + errorMessage = 'Akun Anda diblokir. Hubungi administrator.'; + } else if (error.status === 500) { + errorMessage = 'Terjadi kesalahan pada server. Coba lagi nanti.'; + } + + showModal('error', 'Error!', errorMessage); + } finally { + // Kembalikan button ke state normal + submitBtn.textContent = originalText; + submitBtn.disabled = false; + } +}); + +// Link register +document.getElementById('registerLink').addEventListener('click', function(e) { + e.preventDefault(); + showModal('error', 'Info', 'Halaman Register sedang dalam pengembangan.'); +}); + +// Clear error visual saat user mulai mengetik +document.getElementById('username').addEventListener('input', function() { + this.style.borderColor = ''; +}); + +document.getElementById('password').addEventListener('input', function() { + this.style.borderColor = ''; +}); + +// Optional: Tambahkan visual error pada input +function setInputError(inputId) { + const input = document.getElementById(inputId); + if (input) { + input.style.borderColor = '#ff0080'; + input.style.boxShadow = '0 0 20px rgba(255, 0, 128, 0.3)'; + } +} + +// Optional: Check apakah user sudah login saat buka halaman +window.addEventListener('DOMContentLoaded', function() { + const token = localStorage.getItem('authToken'); + const username = localStorage.getItem('username'); + + // Jika sudah ada token, redirect ke homepage + if (token && username) { + // Optional: Validasi token dulu ke backend + console.log('User sudah login:', username); + // window.location.href = 'homepage.html'; // Uncomment untuk auto redirect + } +}); \ No newline at end of file diff --git a/Modal.js b/Modal.js new file mode 100644 index 0000000..b133453 --- /dev/null +++ b/Modal.js @@ -0,0 +1,50 @@ +export function showModal(type, title, message) { + const modal = document.getElementById('customModal'); + const modalIcon = document.getElementById('modalIcon'); + const modalTitle = document.getElementById('modalTitle'); + const modalMessage = document.getElementById('modalMessage'); + + modalIcon.className = 'modal-icon'; + + if (type === 'success') { + modalIcon.classList.add('success'); + modalTitle.textContent = title || 'Login Berhasil!'; + } else { + modalIcon.classList.add('error'); + modalTitle.textContent = title || 'Login Gagal!'; + } + + modalMessage.textContent = message; + modal.classList.add('show'); +} + +export function closeModal() { + const modal = document.getElementById('customModal'); + modal.classList.remove('show'); +} + +export function setupModalOk() { + document.getElementById('modalOkBtn').addEventListener('click', function() { + const modalIcon = document.getElementById('modalIcon'); + + if (modalIcon.classList.contains('success')) { + window.location.href = 'homepage.html'; + } else { + closeModal(); + } + }); +} + +export function setupOutsideClose() { + document.getElementById('customModal').addEventListener('click', function(e) { + const modalIcon = document.getElementById('modalIcon'); + + if (e.target === this) { + if (modalIcon.classList.contains('success')) { + window.location.href = 'homepage.html'; + } else { + closeModal(); + } + } + }); +} \ No newline at end of file