Merge branch 'main' of https://github.com/Vassshhh/empty
This commit is contained in:
3
package-lock.json
generated
3
package-lock.json
generated
@@ -14101,12 +14101,15 @@
|
|||||||
"node": ">= 0.8.0"
|
"node": ">= 0.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
<<<<<<< HEAD
|
||||||
"node_modules/set-blocking": {
|
"node_modules/set-blocking": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
|
||||||
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
"integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
=======
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
"node_modules/set-cookie-parser": {
|
"node_modules/set-cookie-parser": {
|
||||||
"version": "2.7.1",
|
"version": "2.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz",
|
||||||
|
|||||||
31
src/App.js
31
src/App.js
@@ -1,14 +1,45 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
=======
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
import { BrowserRouter, Routes, Route, Navigate } from "react-router-dom";
|
||||||
|
|
||||||
|
import Dashboard from "./Dashboard";
|
||||||
|
|
||||||
|
import Login from "./Login";
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
import CameraKtp from "./KTPScanner";
|
import CameraKtp from "./KTPScanner";
|
||||||
import Dashboard from "./Dashboard";
|
import Dashboard from "./Dashboard";
|
||||||
|
|
||||||
|
import "./App.css";
|
||||||
|
|
||||||
|
// ✅ Komponen proteksi route
|
||||||
|
const ProtectedRoute = ({ element }) => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
return token ? element : <Navigate to="/login" />;
|
||||||
|
};
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<div className="App">
|
<div className="App">
|
||||||
|
<<<<<<< HEAD
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/" element={<Dashboard />} />
|
<Route path="/" element={<Dashboard />} />
|
||||||
<Route path="/scan" element={<CameraKtp />} />
|
<Route path="/scan" element={<CameraKtp />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
=======
|
||||||
|
<BrowserRouter>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/login" element={<Login />} />
|
||||||
|
<Route path="/" element={<CameraKtp />} />
|
||||||
|
<Route
|
||||||
|
path="/dashboard"
|
||||||
|
element={<ProtectedRoute element={<Dashboard />} />}
|
||||||
|
/>
|
||||||
|
</Routes>
|
||||||
|
</BrowserRouter>
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
273
src/Dashboard.js
273
src/Dashboard.js
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styles from "./Dashboard.module.css";
|
import styles from "./Dashboard.module.css";
|
||||||
import Header from "./components/Header";
|
import Header from "./components/Header";
|
||||||
@@ -17,8 +18,280 @@ const Dashboard = () => {
|
|||||||
</div>
|
</div>
|
||||||
<Chart />
|
<Chart />
|
||||||
</div>
|
</div>
|
||||||
|
=======
|
||||||
|
import React, { useState, useRef, useEffect } from "react";
|
||||||
|
import styles from "./Dashboard.module.css";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
// Pastikan Anda sudah menginstal Recharts: npm install recharts
|
||||||
|
// import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer } from 'recharts'; // Contoh Recharts
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||||
|
const menuRef = useRef(null);
|
||||||
|
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const [selectedRole, setSelectedRole] = useState("officer");
|
||||||
|
const [successMessage, setSuccessMessage] = useState("");
|
||||||
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
|
const [user, setUser] = useState({});
|
||||||
|
const [totalFilesSentToday, setTotalFilesSentToday] = useState(0);
|
||||||
|
const [totalFilesSentMonth, setTotalFilesSentMonth] = useState(0);
|
||||||
|
const [totalFilesSentOverall, setTotalFilesSentOverall] = useState(0);
|
||||||
|
const [officerPerformanceData, setOfficerPerformanceData] = useState([]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const verifyTokenAndFetchData = async () => {
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
if (!token) {
|
||||||
|
window.location.href = "/login";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://bot.kediritechnopark.com/webhook/dashboard/psi",
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || !data[0].payload.username) {
|
||||||
|
throw new Error("Unauthorized");
|
||||||
|
}
|
||||||
|
|
||||||
|
setUser(data[0].payload);
|
||||||
|
|
||||||
|
// Pastikan API Anda mengembalikan data ini, contoh:
|
||||||
|
// data[0].payload.stats = { today: 120, month: 2500, overall: 15000 };
|
||||||
|
// data[0].payload.officerPerformance = [{ name: "Budi", filesSent: 50 }, { name: "Ani", filesSent: 70 }];
|
||||||
|
|
||||||
|
if (data[0].payload.stats) {
|
||||||
|
setTotalFilesSentToday(data[0].payload.stats.today);
|
||||||
|
setTotalFilesSentMonth(data[0].payload.stats.month);
|
||||||
|
setTotalFilesSentOverall(data[0].payload.stats.overall);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data[0].payload.officerPerformance) {
|
||||||
|
setOfficerPerformanceData(data[0].payload.officerPerformance);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Token tidak valid:", error.message);
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
verifyTokenAndFetchData();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleLogout = () => {
|
||||||
|
localStorage.removeItem("token");
|
||||||
|
localStorage.removeItem("user");
|
||||||
|
localStorage.removeItem("role");
|
||||||
|
window.location.reload();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAddOfficer = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
const token = localStorage.getItem("token");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(
|
||||||
|
"https://bot.kediritechnopark.com/webhook/add-officer",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
Authorization: `Bearer ${token}`,
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
role: selectedRole,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
if (!response.ok || data.success === false) {
|
||||||
|
throw new Error(data.message || "Gagal menambahkan officer");
|
||||||
|
}
|
||||||
|
|
||||||
|
setSuccessMessage("Officer berhasil ditambahkan");
|
||||||
|
setUsername("");
|
||||||
|
setPassword("");
|
||||||
|
setSelectedRole("officer");
|
||||||
|
setErrorMessage("");
|
||||||
|
// Pertimbangkan untuk memuat ulang data performa jika penambahan officer baru mempengaruhi grafik
|
||||||
|
} catch (error) {
|
||||||
|
setErrorMessage(error.message || "Gagal menambahkan officer");
|
||||||
|
setSuccessMessage("");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event) => {
|
||||||
|
if (menuRef.current && !menuRef.current.contains(event.target)) {
|
||||||
|
setIsMenuOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.dashboardContainer}>
|
||||||
|
<div className={styles.dashboardHeader}>
|
||||||
|
<div className={styles.logoAndTitle}>
|
||||||
|
<img src="/dermalounge.jpg" alt="Bot Avatar" />
|
||||||
|
<h1 className={styles.h1}>PSI Dashboard</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.dropdownContainer} ref={menuRef}>
|
||||||
|
<span className={styles.userDisplayName}>{user.username || "Guest"}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsMenuOpen(!isMenuOpen)}
|
||||||
|
className={styles.dropdownToggle}
|
||||||
|
aria-expanded={isMenuOpen ? "true" : "false"} /* Aksesibilitas */
|
||||||
|
aria-haspopup="true" /* Aksesibilitas */
|
||||||
|
>
|
||||||
|
☰
|
||||||
|
</button>
|
||||||
|
{isMenuOpen && (
|
||||||
|
<div className={styles.dropdownMenu}>
|
||||||
|
<button
|
||||||
|
onClick={() => { navigate("/profile"); setIsMenuOpen(false); }} /* Tutup menu setelah klik */
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
>
|
||||||
|
Profile
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => { handleLogout(); setIsMenuOpen(false); }} /* Tutup menu setelah klik */
|
||||||
|
className={styles.dropdownItem}
|
||||||
|
>
|
||||||
|
Logout
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.mainContent}>
|
||||||
|
{/* Summary Cards */}
|
||||||
|
<div className={styles.summaryCardsContainer}>
|
||||||
|
<div className={styles.summaryCard}>
|
||||||
|
<h3>Total Hari Ini</h3>
|
||||||
|
<p>{totalFilesSentToday}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryCard}>
|
||||||
|
<h3>Total Bulan Ini</h3>
|
||||||
|
<p>{totalFilesSentMonth}</p>
|
||||||
|
</div>
|
||||||
|
<div className={styles.summaryCard}>
|
||||||
|
<h3>Total Keseluruhan</h3>
|
||||||
|
<p>{totalFilesSentOverall}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Grid for Form (Admin) and Chart (Admin & Officer) */}
|
||||||
|
<div className={styles.dashboardGrid}>
|
||||||
|
{user.role === "admin" && ( /* Render form hanya jika admin */
|
||||||
|
<div className={styles.formSection}>
|
||||||
|
<h2>Tambah Officer Baru</h2>
|
||||||
|
<form onSubmit={handleAddOfficer} className={styles.form}>
|
||||||
|
<label>
|
||||||
|
Username:
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={username}
|
||||||
|
onChange={(e) => setUsername(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Password:
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
value={password}
|
||||||
|
onChange={(e) => setPassword(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
Role:
|
||||||
|
<select
|
||||||
|
value={selectedRole}
|
||||||
|
onChange={(e) => setSelectedRole(e.target.value)}
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<option value="officer">Officer</option>
|
||||||
|
<option value="admin">Admin</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button type="submit" className={styles.submitButton}>
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{successMessage && <p className={styles.success}>{successMessage}</p>}
|
||||||
|
{errorMessage && <p className={styles.error}>{errorMessage}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Chart Section - Visible to both Admin and Officer */}
|
||||||
|
<div className={styles.chartSection}>
|
||||||
|
<h2>Performa Pengiriman File Petugas</h2>
|
||||||
|
{officerPerformanceData.length > 0 ? (
|
||||||
|
// Contoh implementasi Recharts:
|
||||||
|
/*
|
||||||
|
<ResponsiveContainer width="100%" height={300}>
|
||||||
|
<BarChart data={officerPerformanceData} margin={{ top: 5, right: 30, left: 20, bottom: 5 }}>
|
||||||
|
<XAxis dataKey="name" interval={0} angle={-30} textAnchor="end" height={60} />
|
||||||
|
<YAxis />
|
||||||
|
<Tooltip />
|
||||||
|
<Bar
|
||||||
|
dataKey="filesSent"
|
||||||
|
fill={getComputedStyle(document.documentElement).getPropertyValue('--primary-red')}
|
||||||
|
/>
|
||||||
|
</BarChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
*/
|
||||||
|
<div className={styles.chartPlaceholder}>
|
||||||
|
Grafik performa petugas akan ditampilkan di sini.
|
||||||
|
(Integrasikan library grafik seperti Recharts/Chart.js)
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className={styles.warning}>Tidak ada data performa petugas untuk ditampilkan.</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={styles.footer}>© 2025 Kediri Technopark</div>
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
<<<<<<< HEAD
|
||||||
export default Dashboard;
|
export default Dashboard;
|
||||||
|
=======
|
||||||
|
export default Dashboard;
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
<<<<<<< HEAD
|
||||||
.dashboard {
|
.dashboard {
|
||||||
display: flex;
|
display: flex;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -24,3 +25,464 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
/* Variabel Warna */
|
||||||
|
:root {
|
||||||
|
--primary-red: #E53935;
|
||||||
|
--secondary-red: #EF5350;
|
||||||
|
--dark-red: #C62828;
|
||||||
|
--light-gray: #EEEEEE;
|
||||||
|
--dark-gray: #424242;
|
||||||
|
--white: #FFFFFF;
|
||||||
|
--success-green: #4CAF50;
|
||||||
|
--warning-yellow: #FFC107;
|
||||||
|
--text-color-light: #F5F5F5; /* Teks terang di latar belakang gelap */
|
||||||
|
--text-color-dark: #212121; /* Teks gelap di latar belakang terang */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Styles & Reset */
|
||||||
|
* {
|
||||||
|
box-sizing: border-box; /* Pastikan padding dan border tidak menambah lebar elemen */
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; /* Font lebih modern */
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardContainer {
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Header --- */
|
||||||
|
.dashboardHeader {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
color: var(--text-color-light);
|
||||||
|
padding: 1rem; /* Menggunakan rem */
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoAndTitle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0; /* Mencegah logo dan judul mengecil terlalu banyak */
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoAndTitle img {
|
||||||
|
width: 2.5rem; /* Ukuran rem */
|
||||||
|
height: 2.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin-right: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardHeader .h1 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem; /* Ukuran rem untuk mobile */
|
||||||
|
font-weight: bold;
|
||||||
|
white-space: nowrap; /* Mencegah judul patah baris */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dropdown Menu */
|
||||||
|
.dropdownContainer {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.8rem;
|
||||||
|
flex-shrink: 0; /* Mencegah mengecil */
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDisplayName {
|
||||||
|
color: var(--text-color-light);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap; /* Pastikan nama pengguna tidak patah baris */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownToggle {
|
||||||
|
background-color: var(--dark-red);
|
||||||
|
color: var(--text-color-light);
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 0.75rem; /* Padding rem */
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
min-width: 3rem; /* Pastikan tombol tidak terlalu kecil */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownToggle:hover {
|
||||||
|
background-color: var(--secondary-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu {
|
||||||
|
position: absolute;
|
||||||
|
top: calc(100% + 0.5rem); /* Posisikan di bawah tombol dengan jarak */
|
||||||
|
right: 0;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
|
||||||
|
z-index: 10;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 8rem; /* Lebar minimum */
|
||||||
|
overflow: hidden; /* Pastikan item tidak keluar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 0.6rem 0.8rem;
|
||||||
|
text-align: left;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
transition: background-color 0.3s ease;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
white-space: nowrap; /* Mencegah item menu patah baris */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem:hover {
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Main Content --- */
|
||||||
|
.mainContent {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 1.25rem; /* Padding rem */
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1.5rem; /* Jarak antar bagian utama */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Summary Cards Container */
|
||||||
|
.summaryCardsContainer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap; /* Mengizinkan kartu melipat */
|
||||||
|
gap: 1rem; /* Jarak antar kartu */
|
||||||
|
justify-content: center; /* Tengahkan kartu di mobile */
|
||||||
|
padding: 1rem;
|
||||||
|
background-color: var(--white);
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
box-shadow: 0 2px 5px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard {
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid var(--secondary-red);
|
||||||
|
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
flex: 1 1 calc(50% - 0.75rem); /* Di layar mobile, 2 kartu per baris */
|
||||||
|
min-width: 120px; /* Batasan agar tidak terlalu kecil */
|
||||||
|
max-width: 250px; /* Batasan agar tidak terlalu lebar di mobile */
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: var(--dark-red);
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard p {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--dark-gray);
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard Grid for Form and Chart */
|
||||||
|
.dashboardGrid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr; /* Default: satu kolom untuk mobile */
|
||||||
|
gap: 1.5rem; /* Jarak antar bagian */
|
||||||
|
flex-grow: 1; /* Agar grid bisa membesar mengisi sisa ruang */
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection,
|
||||||
|
.chartSection {
|
||||||
|
background-color: var(--white);
|
||||||
|
padding: 1.5rem; /* Padding rem */
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h2,
|
||||||
|
.chartSection h2 {
|
||||||
|
color: var(--dark-red);
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 0.8rem;
|
||||||
|
color: var(--text-color-dark);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input[type="text"],
|
||||||
|
.form input[type="password"],
|
||||||
|
.form select {
|
||||||
|
width: 100%;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: 0.3rem;
|
||||||
|
border: 1px solid var(--light-gray);
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: border-color 0.3s ease, box-shadow 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input[type="text"]:focus,
|
||||||
|
.form input[type="password"]:focus,
|
||||||
|
.form select:focus {
|
||||||
|
border-color: var(--primary-red);
|
||||||
|
box-shadow: 0 0 0 3px rgba(229, 57, 53, 0.2); /* Ring focus */
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
background-color: var(--primary-red);
|
||||||
|
color: var(--white);
|
||||||
|
border: none;
|
||||||
|
padding: 0.8rem 1.5rem;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
width: 100%;
|
||||||
|
transition: background-color 0.3s ease, transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton:hover {
|
||||||
|
background-color: var(--secondary-red);
|
||||||
|
transform: translateY(-2px); /* Efek sedikit naik */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Messages */
|
||||||
|
.success {
|
||||||
|
background-color: #e6ffe6;
|
||||||
|
color: var(--success-green);
|
||||||
|
border: 1px solid var(--success-green);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
background-color: #ffe6e6;
|
||||||
|
color: var(--primary-red);
|
||||||
|
border: 1px solid var(--primary-red);
|
||||||
|
padding: 0.8rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
text-align: center;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
background-color: #fffbe6;
|
||||||
|
color: var(--warning-yellow);
|
||||||
|
border: 1px solid var(--warning-yellow);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: 0.3rem;
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.footer {
|
||||||
|
background-color: var(--dark-red);
|
||||||
|
color: var(--text-color-light);
|
||||||
|
text-align: center;
|
||||||
|
padding: 0.75rem;
|
||||||
|
margin-top: auto;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartPlaceholder {
|
||||||
|
background-color: var(--light-gray);
|
||||||
|
height: 20rem; /* Menggunakan rem */
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--dark-gray);
|
||||||
|
font-style: italic;
|
||||||
|
border-radius: 0.6rem;
|
||||||
|
border: 1px dashed var(--secondary-red);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- Media Queries for Tablets and Desktops --- */
|
||||||
|
|
||||||
|
/* Tablet-sized screens and up */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.dashboardHeader {
|
||||||
|
padding: 1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoAndTitle img {
|
||||||
|
width: 3rem;
|
||||||
|
height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardHeader .h1 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.userDisplayName {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownToggle {
|
||||||
|
padding: 0.6rem 1rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownMenu {
|
||||||
|
min-width: 10rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownItem {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
padding: 1.5rem 2.5rem; /* Padding lebih besar di tablet/desktop */
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCardsContainer {
|
||||||
|
justify-content: flex-start; /* Sejajarkan ke kiri di tablet/desktop */
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard {
|
||||||
|
flex: 1 1 calc(33.33% - 1rem); /* 3 kartu per baris di tablet */
|
||||||
|
max-width: 200px; /* Batasi lebar kartu agar tetap rapi */
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard h3 {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard p {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardGrid {
|
||||||
|
grid-template-columns: 1fr 2fr; /* Dua kolom: form (1/3), chart (2/3) */
|
||||||
|
gap: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection,
|
||||||
|
.chartSection {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h2,
|
||||||
|
.chartSection h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form label {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form input[type="text"],
|
||||||
|
.form input[type="password"],
|
||||||
|
.form select {
|
||||||
|
padding: 0.8rem;
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
padding: 1rem 1.8rem;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.warning {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartPlaceholder {
|
||||||
|
height: 25rem; /* Tinggi placeholder lebih besar di tablet/desktop */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Desktop-sized screens and up */
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
.dashboardHeader {
|
||||||
|
padding: 1.5rem 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logoAndTitle img {
|
||||||
|
width: 3.5rem;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardHeader .h1 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mainContent {
|
||||||
|
padding: 2rem 4rem;
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCardsContainer {
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.summaryCard {
|
||||||
|
flex: 0 0 auto; /* Ukuran tetap di desktop */
|
||||||
|
min-width: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dashboardGrid {
|
||||||
|
gap: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection,
|
||||||
|
.chartSection {
|
||||||
|
padding: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formSection h2,
|
||||||
|
.chartSection h2 {
|
||||||
|
font-size: 1.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.submitButton {
|
||||||
|
padding: 1.1rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chartPlaceholder {
|
||||||
|
height: 30rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jika hanya officer, pastikan chart mengambil seluruh lebar kolom */
|
||||||
|
/* Ini sebenarnya sudah ditangani oleh grid secara default, tapi bisa diperjelas */
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.dashboardGrid > *:only-child { /* Jika hanya ada satu anak elemen di dalam dashboardGrid */
|
||||||
|
grid-column: 1 / -1; /* Ambil seluruh lebar dari kolom pertama hingga terakhir */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>>>>>>> b9b4e4c859bd8face05c8d89d7f0c914d9e84a04
|
||||||
|
|||||||
84
src/Login.js
Normal file
84
src/Login.js
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
import React, { useState } from "react";
|
||||||
|
import styles from "./Login.module.css";
|
||||||
|
|
||||||
|
const Login = () => {
|
||||||
|
const [formData, setFormData] = useState({
|
||||||
|
username: "",
|
||||||
|
password: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const [error, setError] = useState("");
|
||||||
|
|
||||||
|
const handleChange = (e) => {
|
||||||
|
setFormData({ ...formData, [e.target.name]: e.target.value });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const loginResponse = await fetch(
|
||||||
|
"https://bot.kediritechnopark.com/webhook/login/psi",
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(formData),
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const loginDataRaw = await loginResponse.json();
|
||||||
|
const loginData = Array.isArray(loginDataRaw)
|
||||||
|
? loginDataRaw[0]
|
||||||
|
: loginDataRaw;
|
||||||
|
|
||||||
|
if (loginData?.success && loginData?.token) {
|
||||||
|
localStorage.setItem("token", loginData.token);
|
||||||
|
window.location.href = "/dashboard";
|
||||||
|
} else {
|
||||||
|
setError(loginData?.message || "Username atau password salah");
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Login Error:", err);
|
||||||
|
setError("Gagal terhubung ke server");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.loginContainer}>
|
||||||
|
<div className={styles.loginBox}>
|
||||||
|
<img src="/dermalounge.jpg" alt="Logo" className={styles.logo} />
|
||||||
|
<h1 className={styles.h1}>Dermalounge AI Admin Login</h1>
|
||||||
|
<p className={styles.subtitle}>
|
||||||
|
Silakan masuk untuk melanjutkan ke dashboard
|
||||||
|
</p>
|
||||||
|
<form onSubmit={handleSubmit} className={styles.form}>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="username"
|
||||||
|
placeholder="Username"
|
||||||
|
value={formData.username}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="password"
|
||||||
|
placeholder="Password"
|
||||||
|
value={formData.password}
|
||||||
|
onChange={handleChange}
|
||||||
|
className={styles.input}
|
||||||
|
/>
|
||||||
|
{error && <p className={styles.error}>{error}</p>}
|
||||||
|
<button type="submit" className={styles.button}>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div className={styles.footer}>© 2025 Kediri Technopark</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Login;
|
||||||
73
src/Login.module.css
Normal file
73
src/Login.module.css
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
.loginContainer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
height: 100vh;
|
||||||
|
background: #f0f2f5;
|
||||||
|
width: 100vw;
|
||||||
|
}
|
||||||
|
|
||||||
|
.loginBox {
|
||||||
|
background: #fff;
|
||||||
|
padding: 40px;
|
||||||
|
border-radius: 10px;
|
||||||
|
box-shadow: 0 4px 25px rgba(0, 0, 0, 0.1);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
width: 80px;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
color: #333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 14px;
|
||||||
|
color: #777;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
padding: 10px 15px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
background-color: #337f83;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 12px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button:hover {
|
||||||
|
background-color: #3c9a9f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: red;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 20px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #aaa;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user