readme.md
This commit is contained in:
@@ -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
@@ -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
|
```bash
|
||||||
npm run dev
|
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.
|
```text
|
||||||
|
gis-portal/
|
||||||
## Learn More
|
├── chloropleth/ # Fitur peta kloroplet Pontianak (PHP/HTML/JS)
|
||||||
|
├── docker/ # Konfigurasi Dockerfile untuk php, nextjs, & db
|
||||||
To learn more about Next.js, take a look at the following resources:
|
│ ├── db/
|
||||||
|
│ │ └── init.sql # Database schema SQL init
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
│ ├── nextjs/
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
│ └── php/
|
||||||
|
├── parsil-tanah/ # Fitur peta parsil tanah (PHP/HTML/JS)
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
├── sistem-km-next/ # Dasbor Sinergi Umat (Next.js App)
|
||||||
|
│ ├── src/
|
||||||
## Deploy on Vercel
|
│ └── README.md # Dokumentasi ini
|
||||||
|
├── spbu-map/ # Fitur peta sebaran SPBU (PHP/HTML/JS)
|
||||||
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.
|
├── docker-compose.yaml # Main configuration file Docker
|
||||||
|
├── docker-compose.override.yaml # Local override config (Port mapping & bind mounts)
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
├── index.html # Halaman Beranda / Gateway Portal GIS
|
||||||
|
└── README.md -> sistem-km-next/README.md
|
||||||
|
```
|
||||||
|
|||||||
@@ -1,29 +1,105 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { supabase } from '@/lib/supabase';
|
import { supabase } from '@/lib/supabase';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
|
import 'leaflet/dist/leaflet.css';
|
||||||
|
|
||||||
export default function ChangePassword() {
|
export default function ChangePassword() {
|
||||||
const [password, setPassword] = useState('');
|
const [password, setPassword] = useState('');
|
||||||
|
const [confirmPassword, setConfirmPassword] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [error, setError] = useState(null);
|
const [error, setError] = useState(null);
|
||||||
const [user, setUser] = useState(null);
|
const [user, setUser] = useState(null);
|
||||||
|
const [profile, setProfile] = useState(null);
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
|
const [showConfirmPassword, setShowConfirmPassword] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const mapRef = useRef(null);
|
||||||
|
const mapInstanceRef = useRef(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
supabase.auth.getUser().then(({ data }) => {
|
supabase.auth.getUser().then(({ data }) => {
|
||||||
if (!data?.user) router.push('/login');
|
if (!data?.user) {
|
||||||
else setUser(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]);
|
}, [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) => {
|
const handleUpdate = async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
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 });
|
const { error: authError } = await supabase.auth.updateUser({ password });
|
||||||
|
|
||||||
if (authError) {
|
if (authError) {
|
||||||
@@ -48,50 +124,131 @@ export default function ChangePassword() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
|
position: 'relative',
|
||||||
minHeight: '100vh',
|
minHeight: '100vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: '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={{
|
<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',
|
padding: '40px',
|
||||||
borderRadius: '24px',
|
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%',
|
width: '100%',
|
||||||
maxWidth: '400px',
|
maxWidth: '400px',
|
||||||
textAlign: 'center',
|
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>
|
<div style={{
|
||||||
<p style={{ margin: '0 0 24px 0', color: '#ef4444', fontSize: '0.85rem', fontWeight: '500', background: '#fee2e2', padding: '10px', borderRadius: '8px' }}>
|
background: 'var(--primary, #3b82f6)',
|
||||||
<i className="fas fa-lock"></i> Untuk alasan keamanan, Anda wajib mengubah kata sandi default sebelum melanjutkan.
|
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>
|
</p>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
<div style={{ background: '#fee2e2', color: '#ef4444', padding: '10px', borderRadius: '8px', marginBottom: '16px', fontSize: '0.85rem' }}>
|
<div style={{ background: '#fee2e2', color: '#ef4444', padding: '10px', borderRadius: '8px', marginBottom: '16px', fontSize: '0.85rem', fontWeight: '500' }}>
|
||||||
{error}
|
<i className="fas fa-exclamation-circle" style={{ marginRight: '5px' }}></i> {error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleUpdate} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
<form onSubmit={handleUpdate} style={{ display: 'flex', flexDirection: 'column', gap: '16px' }}>
|
||||||
<div style={{ textAlign: 'left' }}>
|
<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' }}>
|
<div style={{ position: 'relative' }}>
|
||||||
<input
|
<input
|
||||||
type={showPassword ? 'text' : 'password'}
|
type={showPassword ? 'text' : 'password'}
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
minLength={6}
|
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '12px 44px 12px 16px', borderRadius: '12px',
|
width: '100%', padding: '12px 44px 12px 16px', borderRadius: '12px',
|
||||||
border: '1px solid #cbd5e1', background: 'white',
|
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)'
|
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
|
required
|
||||||
|
minLength={6}
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -118,19 +275,67 @@ export default function ChangePassword() {
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</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
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
style={{
|
style={{
|
||||||
width: '100%', padding: '14px', borderRadius: '12px',
|
width: '100%', padding: '14px', borderRadius: '12px',
|
||||||
background: '#10b981', color: 'white',
|
background: 'var(--primary, #3b82f6)', color: 'white',
|
||||||
border: 'none', fontWeight: '600', fontSize: '1rem',
|
border: 'none', fontWeight: '600', fontSize: '1rem',
|
||||||
cursor: loading ? 'not-allowed' : 'pointer',
|
cursor: loading ? 'not-allowed' : 'pointer',
|
||||||
marginTop: '8px', transition: 'background 0.2s',
|
marginTop: '8px', transition: 'all 0.2s',
|
||||||
opacity: loading ? 0.7 : 1
|
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>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
export default function ProfileModal({
|
export default function ProfileModal({
|
||||||
showProfileModal,
|
showProfileModal,
|
||||||
@@ -8,6 +9,7 @@ export default function ProfileModal({
|
|||||||
handleLogout,
|
handleLogout,
|
||||||
isLoggingOut
|
isLoggingOut
|
||||||
}) {
|
}) {
|
||||||
|
const router = useRouter();
|
||||||
if (!showProfileModal) return null;
|
if (!showProfileModal) return null;
|
||||||
|
|
||||||
const getRoleBadge = (role) => {
|
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)'}
|
<i className="fas fa-sign-out-alt"></i> {isLoggingOut ? 'Keluar...' : 'Keluar dari Akun (Logout)'}
|
||||||
</button>
|
</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
|
<button
|
||||||
onClick={() => setShowProfileModal(false)}
|
onClick={() => setShowProfileModal(false)}
|
||||||
className="btn btn-outline"
|
className="btn btn-outline"
|
||||||
|
|||||||
Reference in New Issue
Block a user