readme.md

This commit is contained in:
Monarch055
2026-06-13 12:04:04 +07:00
parent 14b8549eee
commit db6c6efd28
4 changed files with 482 additions and 52 deletions
+116
View File
@@ -0,0 +1,116 @@
# GIS Portal & Sinergi Umat Dashboard
Proyek ini adalah portal Sistem Informasi Geografis (GIS) terintegrasi yang menggabungkan visualisasi peta spasial Pontianak dengan sistem dasbor manajemen kesejahteraan masyarakat (Sinergi Umat).
---
## 🏛️ Komponen Proyek
1. **Portal Utama GIS (PHP + Apache)**:
- Visualisasi Lokasi SPBU (`spbu-map`)
- Visualisasi Parsil Tanah (`parsil-tanah`)
- Peta Kloroplet (`chloropleth`)
2. **Dasbor Sinergi Umat (`sistem-km-next`)**:
- Aplikasi berbasis Next.js untuk pemetaan rumah ibadah (Mosque, Church, Vihara, Temple) dan survei kesejahteraan Keluarga Penerima Manfaat (KPM).
3. **Database MySQL**:
- Penyimpanan data SPBU, parsil, dan kloroplet.
4. **Supabase**:
- Backend auth dan database real-time untuk data rumah ibadah, data keluarga (households), transaksi donasi, dan approval queue di Sinergi Umat.
---
## 🐳 Opsi 1: Menjalankan Menggunakan Docker (Direkomendasikan)
Dengan Docker Compose, seluruh aplikasi portal PHP, dasbor Next.js, MySQL database, dan phpMyAdmin akan dijalankan otomatis dalam kontainer yang saling terhubung.
### Prasyarat
- Sudah menginstal **Docker Desktop** di komputer Anda.
### Langkah-langkah
1. Buka terminal/command prompt pada **direktori root** proyek (`gis-portal` / folder tempat `docker-compose.yaml` berada).
2. Jalankan perintah berikut untuk mengunduh, membuat build image, dan menjalankan kontainer secara background:
```bash
docker compose up -d --build
```
3. Setelah proses build selesai dan container berjalan (`healthy`), Anda dapat mengakses layanan berikut di browser Anda:
- **Portal Utama GIS (PHP, SPBU, Parsil, Kloroplet)**: [http://localhost:8080](http://localhost:8080)
- **Dasbor Sinergi Umat (Next.js)**: [http://localhost:3000](http://localhost:3000)
- **phpMyAdmin (Manajemen Database MySQL)**: [http://localhost:8085](http://localhost:8085)
- **MySQL Database**: Berjalan di port `3306` (host: `localhost`, user: `root`, password: `rootpassword`, database: `spbu_db`).
### Catatan Penting Docker:
- Data MySQL disimpan secara persisten di volume docker `db-data`.
- Skema awal database otomatis di-import saat database pertama kali di-boot dari berkas `./docker/db/init.sql`.
---
## 🔧 Opsi 2: Menjalankan Tanpa Docker (Manual)
Jika ingin menjalankan aplikasi secara langsung di sistem lokal Anda tanpa menggunakan kontainer Docker.
### Prasyarat
- **Web Server Lokal** (seperti Laragon atau XAMPP) yang mendukung PHP (min v8.1) dan MySQL.
- **Node.js** (min v18 atau v20) terinstal pada komputer Anda.
### Langkah-langkah Setup Manual:
#### 1. Setup Database MySQL Lokal
1. Pastikan server MySQL lokal Anda (Laragon/XAMPP) sudah menyala.
2. Buat database baru bernama `spbu_db` melalui phpMyAdmin lokal Anda.
3. Import berkas SQL skema awal yang terletak di `docker/db/init.sql` ke dalam database `spbu_db` tersebut.
#### 2. Jalankan Portal GIS Utama (PHP App)
1. Pindahkan atau hubungkan (symlink) seluruh folder proyek `gis-portal` ke direktori root web server Anda:
- Untuk **Laragon**: taruh di `C:\laragon\www\gis-portal`
- Untuk **XAMPP**: taruh di `C:\xampp\htdocs\gis-portal`
2. Konfigurasi koneksi database PHP berada di berkas:
- `spbu-map/db.php`
- `parsil-tanah/db.php`
*(Secara default, skrip PHP akan otomatis terhubung ke localhost dengan database `spbu_db`, user `root`, dan password kosong `""`)*.
3. Akses portal utama melalui browser Anda di:
- Laragon: [http://gis-portal.test](http://gis-portal.test)
- XAMPP: [http://localhost/gis-portal](http://localhost/gis-portal)
#### 3. Jalankan Dasbor Sinergi Umat (Next.js App)
1. Buka terminal Anda dan masuk ke direktori Next.js:
```bash
cd sistem-km-next
```
2. Instal pustaka ketergantungan (dependencies) dengan perintah:
```bash
npm install
```
3. Pastikan berkas `.env.local` sudah ada di dalam folder `sistem-km-next` dan berisi kredensial Supabase berikut (sudah otomatis disertakan dalam berkas zip):
```env
NEXT_PUBLIC_SUPABASE_URL=https://gzpvygrsfdtzykyqbnes.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
4. Jalankan server pengembangan Next.js lokal:
```bash
npm run dev
```
5. Buka browser Anda dan akses dasbor Sinergi Umat di [http://localhost:3000](http://localhost:3000).
---
## 💾 Struktur Folder Proyek
```text
gis-portal/
├── README.md # Dokumentasi utama (berkas ini)
├── chloropleth/ # Fitur peta kloroplet Pontianak (PHP/HTML/JS)
├── docker/ # Konfigurasi Dockerfile untuk php, nextjs, & db
│ ├── db/
│ │ └── init.sql # Database schema SQL init
│ ├── nextjs/
│ └── php/
├── parsil-tanah/ # Fitur peta parsil tanah (PHP/HTML/JS)
├── sistem-km-next/ # Dasbor Sinergi Umat (Next.js App)
│ ├── src/
│ └── README.md # Dokumentasi subsistem Next.js
├── spbu-map/ # Fitur peta sebaran SPBU (PHP/HTML/JS)
├── docker-compose.yaml # Main configuration file Docker
├── docker-compose.override.yaml # Local override config (Port mapping & bind mounts)
└── index.html # Halaman Beranda / Gateway Portal GIS
```
+107 -27
View File
@@ -1,36 +1,116 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# GIS Portal & Sinergi Umat Dashboard
## Getting Started
Proyek ini adalah portal Sistem Informasi Geografis (GIS) terintegrasi yang menggabungkan visualisasi peta spasial Pontianak dengan sistem dasbor manajemen kesejahteraan masyarakat (Sinergi Umat).
First, run the development server:
---
## 🏛️ Komponen Proyek
1. **Portal Utama GIS (PHP + Apache)**:
- Visualisasi Lokasi SPBU (`spbu-map`)
- Visualisasi Parsil Tanah (`parsil-tanah`)
- Peta Kloroplet (`chloropleth`)
2. **Dasbor Sinergi Umat (`sistem-km-next`)**:
- Aplikasi berbasis Next.js untuk pemetaan rumah ibadah (Mosque, Church, Vihara, Temple) dan survei kesejahteraan Keluarga Penerima Manfaat (KPM).
3. **Database MySQL**:
- Penyimpanan data SPBU, parsil, dan kloroplet.
4. **Supabase**:
- Backend auth dan database real-time untuk data rumah ibadah, data keluarga (households), transaksi donasi, dan approval queue di Sinergi Umat.
---
## 🐳 Opsi 1: Menjalankan Menggunakan Docker (Direkomendasikan)
Dengan Docker Compose, seluruh aplikasi portal PHP, dasbor Next.js, MySQL database, dan phpMyAdmin akan dijalankan otomatis dalam kontainer yang saling terhubung.
### Prasyarat
- Sudah menginstal **Docker Desktop** di komputer Anda.
### Langkah-langkah
1. Buka terminal/command prompt pada **direktori root** proyek (`gis-portal` / folder tempat `docker-compose.yaml` berada).
2. Jalankan perintah berikut untuk mengunduh, membuat build image, dan menjalankan kontainer secara background:
```bash
docker compose up -d --build
```
3. Setelah proses build selesai dan container berjalan (`healthy`), Anda dapat mengakses layanan berikut di browser Anda:
- **Portal Utama GIS (PHP, SPBU, Parsil, Kloroplet)**: [http://localhost:8080](http://localhost:8080)
- **Dasbor Sinergi Umat (Next.js)**: [http://localhost:3000](http://localhost:3000)
- **phpMyAdmin (Manajemen Database MySQL)**: [http://localhost:8085](http://localhost:8085)
- **MySQL Database**: Berjalan di port `3306` (host: `localhost`, user: `root`, password: `rootpassword`, database: `spbu_db`).
### Catatan Penting Docker:
- Data MySQL disimpan secara persisten di volume docker `db-data`.
- Skema awal database otomatis di-import saat database pertama kali di-boot dari berkas `./docker/db/init.sql`.
---
## 🔧 Opsi 2: Menjalankan Tanpa Docker (Manual)
Jika ingin menjalankan aplikasi secara langsung di sistem lokal Anda tanpa menggunakan kontainer Docker.
### Prasyarat
- **Web Server Lokal** (seperti Laragon atau XAMPP) yang mendukung PHP (min v8.1) dan MySQL.
- **Node.js** (min v18 atau v20) terinstal pada komputer Anda.
### Langkah-langkah Setup Manual:
#### 1. Setup Database MySQL Lokal
1. Pastikan server MySQL lokal Anda (Laragon/XAMPP) sudah menyala.
2. Buat database baru bernama `spbu_db` melalui phpMyAdmin lokal Anda.
3. Import berkas SQL skema awal yang terletak di `docker/db/init.sql` ke dalam database `spbu_db` tersebut.
#### 2. Jalankan Portal GIS Utama (PHP App)
1. Pindahkan atau hubungkan (symlink) seluruh folder proyek `gis-portal` ke direktori root web server Anda:
- Untuk **Laragon**: taruh di `C:\laragon\www\gis-portal`
- Untuk **XAMPP**: taruh di `C:\xampp\htdocs\gis-portal`
2. Konfigurasi koneksi database PHP berada di berkas:
- `spbu-map/db.php`
- `parsil-tanah/db.php`
*(Secara default, skrip PHP akan otomatis terhubung ke localhost dengan database `spbu_db`, user `root`, dan password kosong `""`)*.
3. Akses portal utama melalui browser Anda di:
- Laragon: [http://gis-portal.test](http://gis-portal.test)
- XAMPP: [http://localhost/gis-portal](http://localhost/gis-portal)
#### 3. Jalankan Dasbor Sinergi Umat (Next.js App)
1. Buka terminal Anda dan masuk ke direktori Next.js:
```bash
cd sistem-km-next
```
2. Instal pustaka ketergantungan (dependencies) dengan perintah:
```bash
npm install
```
3. Pastikan berkas `.env.local` sudah ada di dalam folder `sistem-km-next` dan berisi kredensial Supabase berikut (sudah otomatis disertakan dalam berkas zip):
```env
NEXT_PUBLIC_SUPABASE_URL=https://gzpvygrsfdtzykyqbnes.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
```
4. Jalankan server pengembangan Next.js lokal:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
```
5. Buka browser Anda dan akses dasbor Sinergi Umat di [http://localhost:3000](http://localhost:3000).
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
---
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
## 💾 Struktur Folder Proyek
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```text
gis-portal/
├── chloropleth/ # Fitur peta kloroplet Pontianak (PHP/HTML/JS)
├── docker/ # Konfigurasi Dockerfile untuk php, nextjs, & db
│ ├── db/
│ │ └── init.sql # Database schema SQL init
│ ├── nextjs/
│ └── php/
├── parsil-tanah/ # Fitur peta parsil tanah (PHP/HTML/JS)
├── sistem-km-next/ # Dasbor Sinergi Umat (Next.js App)
│ ├── src/
│ └── README.md # Dokumentasi ini
├── spbu-map/ # Fitur peta sebaran SPBU (PHP/HTML/JS)
├── docker-compose.yaml # Main configuration file Docker
├── docker-compose.override.yaml # Local override config (Port mapping & bind mounts)
├── index.html # Halaman Beranda / Gateway Portal GIS
└── README.md -> sistem-km-next/README.md
```
+225 -20
View File
@@ -1,29 +1,105 @@
'use client';
import { useState, useEffect } from 'react';
import { useState, useEffect, useRef } from 'react';
import { supabase } from '@/lib/supabase';
import { useRouter } from 'next/navigation';
import 'leaflet/dist/leaflet.css';
export default function ChangePassword() {
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const [user, setUser] = useState(null);
const [profile, setProfile] = useState(null);
const [showPassword, setShowPassword] = useState(false);
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
const router = useRouter();
const mapRef = useRef(null);
const mapInstanceRef = useRef(null);
useEffect(() => {
supabase.auth.getUser().then(({ data }) => {
if (!data?.user) router.push('/login');
else setUser(data.user);
if (!data?.user) {
router.push('/login');
} else {
setUser(data.user);
supabase
.from('profiles')
.select('*')
.eq('id', data.user.id)
.single()
.then(({ data: profileData }) => {
setProfile(profileData);
});
}
});
}, [router]);
useEffect(() => {
let active = true;
// Dynamically load Leaflet for the background map
import('leaflet').then((L) => {
if (!active) return;
if (!mapInstanceRef.current && mapRef.current) {
// Initialize map centered on Pontianak
const map = L.map(mapRef.current, {
center: [-0.0227, 109.3340],
zoom: 13,
zoomControl: false,
dragging: false,
scrollWheelZoom: false,
doubleClickZoom: false,
boxZoom: false,
keyboard: false,
attributionControl: false
});
// Use CartoDB Positron for a clean, light background
L.tileLayer('https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png', {
maxZoom: 19,
subdomains: 'abcd'
}).addTo(map);
mapInstanceRef.current = map;
// Force resize recalculation to ensure tiles render immediately
setTimeout(() => {
if (active && mapInstanceRef.current) {
mapInstanceRef.current.invalidateSize();
}
}, 150);
}
});
return () => {
active = false;
if (mapInstanceRef.current) {
mapInstanceRef.current.remove();
mapInstanceRef.current = null;
}
};
}, []);
const handleUpdate = async (e) => {
e.preventDefault();
setLoading(true);
setError(null);
if (password !== confirmPassword) {
setError('Konfirmasi kata sandi baru tidak cocok.');
setLoading(false);
return;
}
if (password.length < 6) {
setError('Kata sandi baru minimal harus 6 karakter.');
setLoading(false);
return;
}
const { error: authError } = await supabase.auth.updateUser({ password });
if (authError) {
@@ -48,50 +124,131 @@ export default function ChangePassword() {
return (
<div style={{
position: 'relative',
minHeight: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
background: '#f8fafc',
fontFamily: '"Inter", sans-serif'
fontFamily: '"Inter", sans-serif',
overflow: 'hidden'
}}>
{/* Return to Dashboard Button */}
<button
onClick={() => router.push('/')}
style={{
position: 'absolute',
top: '24px',
left: '24px',
zIndex: 10,
background: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
border: '1px solid rgba(255, 255, 255, 0.6)',
borderRadius: '14px',
padding: '10px 18px',
display: 'flex',
alignItems: 'center',
gap: '8px',
color: '#1e293b',
fontWeight: '600',
fontSize: '0.9rem',
cursor: 'pointer',
boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.15)',
transition: 'all 0.2s',
outline: 'none'
}}
onMouseOver={(e) => {
e.currentTarget.style.transform = 'translateY(-2px)';
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.95)';
}}
onMouseOut={(e) => {
e.currentTarget.style.transform = 'translateY(0)';
e.currentTarget.style.background = 'rgba(255, 255, 255, 0.85)';
}}
>
<i className="fas fa-arrow-left" style={{ color: 'var(--primary, #3b82f6)' }}></i>
<span>Kembali ke Dasbor</span>
</button>
{/* Dynamic Leaflet Background */}
<div
ref={mapRef}
style={{
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
width: '100%',
height: '100%',
zIndex: 1,
filter: 'brightness(0.95)'
}}
/>
{/* Glassmorphism Dark/Blur Overlay */}
<div style={{
background: 'white',
position: 'absolute',
top: 0, left: 0, right: 0, bottom: 0,
zIndex: 2,
background: 'rgba(15, 23, 42, 0.45)',
backdropFilter: 'blur(4px)',
WebkitBackdropFilter: 'blur(4px)'
}} />
{/* Change Password Card */}
<div style={{
background: 'rgba(255, 255, 255, 0.85)',
backdropFilter: 'blur(8px)',
WebkitBackdropFilter: 'blur(8px)',
padding: '40px',
borderRadius: '24px',
boxShadow: '0 8px 32px rgba(0, 0, 0, 0.05)',
boxShadow: '0 25px 50px -12px rgba(0, 0, 0, 0.5)',
width: '100%',
maxWidth: '400px',
textAlign: 'center',
border: '1px solid #e2e8f0'
border: '1px solid rgba(255, 255, 255, 0.6)',
zIndex: 10
}}>
<h2 style={{ margin: '0 0 8px 0', color: '#1e293b', fontSize: '1.5rem' }}>Rotasi Keamanan</h2>
<p style={{ margin: '0 0 24px 0', color: '#ef4444', fontSize: '0.85rem', fontWeight: '500', background: '#fee2e2', padding: '10px', borderRadius: '8px' }}>
<i className="fas fa-lock"></i> Untuk alasan keamanan, Anda wajib mengubah kata sandi default sebelum melanjutkan.
<div style={{
background: 'var(--primary, #3b82f6)',
width: '60px', height: '60px',
borderRadius: '50%',
display: 'flex', alignItems: 'center', justifyContent: 'center',
color: 'white', fontSize: '24px',
margin: '0 auto 20px auto',
boxShadow: '0 10px 15px -3px rgba(59, 130, 246, 0.5)'
}}>
<i className="fas fa-key"></i>
</div>
<h2 style={{ margin: '0 0 8px 0', color: '#1e293b', fontSize: '1.8rem', fontWeight: '800' }}>Ubah Kata Sandi</h2>
<p style={{ margin: '0 0 24px 0', color: '#64748b', fontSize: '0.95rem' }}>
{profile?.password_changed === false
? 'Anda wajib mengubah kata sandi bawaan demi keamanan akun.'
: 'Ubah kata sandi Anda untuk meningkatkan keamanan akun.'}
</p>
{error && (
<div style={{ background: '#fee2e2', color: '#ef4444', padding: '10px', borderRadius: '8px', marginBottom: '16px', fontSize: '0.85rem' }}>
{error}
<div style={{ background: '#fee2e2', color: '#ef4444', padding: '10px', borderRadius: '8px', marginBottom: '16px', fontSize: '0.85rem', fontWeight: '500' }}>
<i className="fas fa-exclamation-circle" style={{ marginRight: '5px' }}></i> {error}
</div>
)}
<form onSubmit={handleUpdate} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
<div style={{ textAlign: 'left' }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#475569', marginBottom: '6px', fontWeight: '500' }}>Kata Sandi Baru</label>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#475569', marginBottom: '6px', fontWeight: '600' }}>Kata Sandi Baru</label>
<div style={{ position: 'relative' }}>
<input
type={showPassword ? 'text' : 'password'}
value={password}
onChange={(e) => setPassword(e.target.value)}
minLength={6}
style={{
width: '100%', padding: '12px 44px 12px 16px', borderRadius: '12px',
border: '1px solid #cbd5e1', background: 'white',
outline: 'none', transition: 'border-color 0.2s', color: 'black',
outline: 'none', transition: 'all 0.2s', color: '#1e293b',
boxShadow: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)'
}}
onFocus={(e) => e.target.style.borderColor = 'var(--primary, #3b82f6)'}
onBlur={(e) => e.target.style.borderColor = '#cbd5e1'}
required
minLength={6}
/>
<button
type="button"
@@ -118,19 +275,67 @@ export default function ChangePassword() {
</button>
</div>
</div>
<div style={{ textAlign: 'left' }}>
<label style={{ display: 'block', fontSize: '0.85rem', color: '#475569', marginBottom: '6px', fontWeight: '600' }}>Konfirmasi Kata Sandi Baru</label>
<div style={{ position: 'relative' }}>
<input
type={showConfirmPassword ? 'text' : 'password'}
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
style={{
width: '100%', padding: '12px 44px 12px 16px', borderRadius: '12px',
border: '1px solid #cbd5e1', background: 'white',
outline: 'none', transition: 'all 0.2s', color: '#1e293b',
boxShadow: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)'
}}
onFocus={(e) => e.target.style.borderColor = 'var(--primary, #3b82f6)'}
onBlur={(e) => e.target.style.borderColor = '#cbd5e1'}
required
minLength={6}
/>
<button
type="button"
onClick={() => setShowConfirmPassword(!showConfirmPassword)}
style={{
position: 'absolute',
right: '14px',
top: '50%',
transform: 'translateY(-50%)',
background: 'none',
border: 'none',
color: '#94a3b8',
cursor: 'pointer',
padding: '4px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '1rem',
outline: 'none'
}}
title={showConfirmPassword ? 'Sembunyikan Kata Sandi' : 'Tampilkan Kata Sandi'}
>
<i className={showConfirmPassword ? 'fas fa-eye-slash' : 'fas fa-eye'}></i>
</button>
</div>
</div>
<button
type="submit"
disabled={loading}
style={{
width: '100%', padding: '14px', borderRadius: '12px',
background: '#10b981', color: 'white',
background: 'var(--primary, #3b82f6)', color: 'white',
border: 'none', fontWeight: '600', fontSize: '1rem',
cursor: loading ? 'not-allowed' : 'pointer',
marginTop: '8px', transition: 'background 0.2s',
opacity: loading ? 0.7 : 1
marginTop: '8px', transition: 'all 0.2s',
opacity: loading ? 0.8 : 1,
boxShadow: loading ? 'none' : '0 4px 6px -1px rgba(59, 130, 246, 0.5)'
}}
onMouseOver={(e) => { if(!loading) e.target.style.transform = 'translateY(-2px)' }}
onMouseOut={(e) => { if(!loading) e.target.style.transform = 'translateY(0)' }}
>
{loading ? 'Menyimpan...' : 'Simpan & Lanjutkan'}
{loading ? <><i className="fas fa-spinner fa-spin"></i> Menyimpan...</> : 'Simpan & Lanjutkan'}
</button>
</form>
</div>
@@ -1,4 +1,5 @@
import React from 'react';
import { useRouter } from 'next/navigation';
export default function ProfileModal({
showProfileModal,
@@ -8,6 +9,7 @@ export default function ProfileModal({
handleLogout,
isLoggingOut
}) {
const router = useRouter();
if (!showProfileModal) return null;
const getRoleBadge = (role) => {
@@ -138,6 +140,33 @@ export default function ProfileModal({
>
<i className="fas fa-sign-out-alt"></i> {isLoggingOut ? 'Keluar...' : 'Keluar dari Akun (Logout)'}
</button>
<button
onClick={() => {
setShowProfileModal(false);
router.push('/change-password');
}}
className="btn"
style={{
background: 'linear-gradient(135deg, var(--primary, #3b82f6), #1d4ed8)',
color: 'white',
border: 'none',
padding: '12px',
borderRadius: '12px',
fontWeight: '600',
fontSize: '0.95rem',
cursor: 'pointer',
boxShadow: '0 4px 12px rgba(59, 130, 246, 0.3)',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
gap: '8px',
transition: 'all 0.2s'
}}
onMouseOver={(e) => e.currentTarget.style.transform = 'translateY(-2px)'}
onMouseOut={(e) => e.currentTarget.style.transform = 'translateY(0)'}
>
<i className="fas fa-key"></i> Ubah Kata Sandi
</button>
<button
onClick={() => setShowProfileModal(false)}
className="btn btn-outline"