This commit is contained in:
Vassshhh
2025-06-17 21:31:52 +07:00
parent 2ed6ecfe75
commit 64f5609d2c
8 changed files with 262 additions and 43 deletions

View File

@@ -2,14 +2,14 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8" /> <meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" /> <link rel="icon" href="%PUBLIC_URL%/dermalounge.jpg" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" /> <meta name="theme-color" content="#000000" />
<meta <meta
name="description" name="description"
content="Web site created using create-react-app" content="Realtime AI Customer Service"
/> />
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" /> <link rel="apple-touch-icon" href="%PUBLIC_URL%/dermalounge.jpg" />
<!-- <!--
manifest.json provides metadata used when your web app is installed on a manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,7 +24,7 @@
work correctly both with client-side routing and a non-root public URL. work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`. Learn how to configure a non-root public URL by running `npm run build`.
--> -->
<title>React App</title> <title>Dermalounge</title>
</head> </head>
<body> <body>
<noscript>You need to enable JavaScript to run this app.</noscript> <noscript>You need to enable JavaScript to run this app.</noscript>

7
public/sw.js Normal file
View File

@@ -0,0 +1,7 @@
self.addEventListener('push', event => {
const data = event.data.json();
self.registration.showNotification(data.title, {
body: data.body,
icon: '/dermalounge.jpg',
});
});

View File

@@ -3,6 +3,7 @@ import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-
import axios from 'axios'; import axios from 'axios';
import Dashboard from './Dashboard'; import Dashboard from './Dashboard';
import ResetPassword from './ResetPassword'; // ⬅️ import komponen reset
import TenantDashboard from './TenantDashboard'; import TenantDashboard from './TenantDashboard';
import ChatBot from './ChatBot'; import ChatBot from './ChatBot';
import Login from './Login'; import Login from './Login';
@@ -49,6 +50,7 @@ function App() {
<Routes> <Routes>
<Route path="/" element={<ChatBotWrapper />} /> <Route path="/" element={<ChatBotWrapper />} />
<Route path="/login" element={<Login />} /> <Route path="/login" element={<Login />} />
<Route path="/reset-password" element={<ResetPassword />} />
{/* ✅ Route /dashboard diproteksi */} {/* ✅ Route /dashboard diproteksi */}
<Route <Route
path="/dashboard" path="/dashboard"

View File

@@ -18,6 +18,11 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
const [input, setInput] = useState(''); const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [isPoppedUp, setIsPoppedUp] = useState(false);
const [name, setName] = useState('');
const [phoneNumber, setPhoneNumber] = useState('');
useEffect(() => { useEffect(() => {
if (existingConversation && existingConversation.length > 0) { if (existingConversation && existingConversation.length > 0) {
@@ -41,7 +46,7 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
} }
}, []); }, []);
const sendMessage = async (textOverride = null, tryCount = 0) => { const sendMessage = async (textOverride = null, name, phoneNumber, tryCount = 0) => {
const message = textOverride || input.trim(); const message = textOverride || input.trim();
if (message === '') return; if (message === '') return;
@@ -60,7 +65,7 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
const response = await fetch('https://bot.kediritechnopark.com/webhook/master-agent/ask/dev', { const response = await fetch('https://bot.kediritechnopark.com/webhook/master-agent/ask/dev', {
method: 'POST', method: 'POST',
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ pertanyaan: message, sessionId: JSON.parse(localStorage.getItem('session')).sessionId, lastSeen: new Date().toISOString() }), body: JSON.stringify({ pertanyaan: message, sessionId: JSON.parse(localStorage.getItem('session')).sessionId, lastSeen: new Date().toISOString(), name: JSON.parse(localStorage.getItem('session')).name, phoneNumber: JSON.parse(localStorage.getItem('session')).phoneNumber }),
}); });
const data = await response.json(); const data = await response.json();
@@ -74,6 +79,12 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
...prev, ...prev,
{ sender: 'bot', text: botAnswer, time: getTime() }, { sender: 'bot', text: botAnswer, time: getTime() },
]); ]);
const session = JSON.parse(localStorage.getItem('session'));
if ((!session || !session.name || !session.phoneNumber) && messages.length > 2) {
setIsPoppedUp(true); // munculkan form input
}
setIsLoading(false); setIsLoading(false);
} catch (error) { } catch (error) {
@@ -87,7 +98,7 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
setIsLoading(false); setIsLoading(false);
return; return;
} }
setTimeout(() => sendMessage(message, tryCount + 1), 3000); setTimeout(() => sendMessage(message, name, phoneNumber, tryCount + 1), 3000);
console.error('Fetch error:', error); console.error('Fetch error:', error);
} }
@@ -157,40 +168,68 @@ const ChatBot = ({ existingConversation, readOnly, hh }) => {
onKeyDown={(e) => e.key === 'Enter' && sendMessage()} onKeyDown={(e) => e.key === 'Enter' && sendMessage()}
disabled={isLoading} disabled={isLoading}
/> />
<button onClick={() => sendMessage()} disabled={isLoading}> <button onClick={() => sendMessage()} disabled={isLoading}>
Kirim Kirim
</button> </button>
</div> </div>
<div className={styles.PopUp}> {isPoppedUp &&
<div className={styles.PopUp}>
<div className={`${styles.message} ${styles['bot']}`}> <div className={`${styles.message} ${styles['bot']}`}>
Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda? Untuk bisa membantu Anda lebih jauh, boleh saya tahu nama dan nomor telepon Anda?
Informasi ini juga membantu tim admin kami jika perlu melakukan follow-up nantinya 😊 Informasi ini juga membantu tim admin kami jika perlu melakukan follow-up nantinya 😊
<div className={styles.quickReplies} style={{ flexDirection: 'column' }}> <div className={styles.quickReplies} style={{ flexDirection: 'column' }}>
<input
className={styles.quickReply}
placeholder="Nama Lengkapmu"
onFocus={() => console.log('Nama focused')}
/>
<div className={styles.inputGroup}>
<span className={styles.prefix}>+62</span>
<input <input
type="tel"
className={styles.quickReply} className={styles.quickReply}
placeholder="Nomor HP" placeholder="Nama Lengkapmu"
onFocus={() => console.log('Telepon focused')} onFocus={() => console.log('Nama focused')}
style={{border: 0, width: '100%'}} value={name}
onChange={(e) => setName(e.target.value)}
maxLength={40}
/> />
</div> <div className={styles.inputGroup}>
<span className={styles.prefix}>+62</span>
<input
type="text"
inputMode="numeric"
pattern="[0-9]*"
maxLength={11}
className={styles.quickReply2}
placeholder="Nomor HP"
value={phoneNumber}
onChange={(e) => {
const value = e.target.value;
// Hanya angka, maksimal 11 karakter
if (/^\d{0,11}$/.test(value)) {
setPhoneNumber(value);
}
}}
onFocus={() => console.log('Telepon focused')}
/>
<div </div>
className={styles.quickReply}
> <div
Lanjut className={styles.quickReply}
style={{ color: name.length > 2 && phoneNumber.length >= 10 ? 'black' : '#ccc' }}
onClick={() => {
if (name.length > 2 && phoneNumber.length >= 10) {
const sessionData = JSON.parse(localStorage.getItem('session')) || {};
sessionData.name = name;
sessionData.phoneNumber = phoneNumber;
localStorage.setItem('session', JSON.stringify(sessionData));
setIsPoppedUp(false)
}
}}
>
Lanjut
</div>
</div> </div>
</div> </div>
</div> </div>
</div> }
</div> </div>
); );
}; };

View File

@@ -129,18 +129,17 @@
.inputGroup { .inputGroup {
display: flex; display: flex;
align-items: center; align-items: center;
border: 1px solid #ccc;
border-radius: 20px;
overflow: hidden;
width: 100%; width: 100%;
} }
.prefix { .prefix {
background-color: #f0f0f0; background-color: #f0f0f0;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
border-right: 1px solid #ccc; border-radius: 20px 0px 0px 20px;
font-size: 12px; font-size: 12px;
color: #555; color: #555;
border: 1px solid #ccc;
border-right: 0;
} }
.quickReply { .quickReply {
@@ -158,3 +157,30 @@
color: white; color: white;
border-color: #075e54; border-color: #075e54;
} }
.quickReply:hover::placeholder {
color: white;
}
.quickReply2 {
width: 100%;
background: #fff;
border: 1px solid #ccc;
padding: 8px 14px;
border-radius: 0 20px 20px 0;
border: 1px solid #ccc;
font-size: 14px;
cursor: pointer;
transition: all 0.2s ease;
}
.quickReply2:hover {
background: #075e54;
color: white;
border-color: #075e54;
border: 1px solid #075e54;
}
.quickReply2:hover::placeholder {
color: white;
}

View File

@@ -18,6 +18,7 @@ const Dashboard = () => {
const [modalContent, setModalContent] = useState(null); const [modalContent, setModalContent] = useState(null);
const [rawData, setRawData] = useState([]); const [rawData, setRawData] = useState([]);
const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading const [loading, setLoading] = useState(true); // ⬅️ Tambahkan state loading
const [checkOnce, setCheckOnce] = useState(false); // ⬅️ Tambahkan state loading
const [stats, setStats] = useState({ const [stats, setStats] = useState({
totalChats: 0, totalChats: 0,
@@ -124,9 +125,49 @@ const Dashboard = () => {
} }
}; };
fetchData(); if (!checkOnce && 'serviceWorker' in navigator) {
subscribeUser();
setCheckOnce(false);
}
fetchData(); // Jalankan langsung saat komponen di-mount
const interval = setInterval(fetchData, 30000); // Jalankan setiap 30 detik
return () => clearInterval(interval); // Bersihkan interval saat komponen unmount
}, [navigate]); }, [navigate]);
const subscribeUser = async () => {
const registration = await navigator.serviceWorker.register('/sw.js', {
scope: '/',
});
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: urlBase64ToUint8Array('BPT-ypQB0Z7HndmeFhRR7AMjDujCLSbOQ21VoVHLQg9MOfWhEZ7SKH5cMjLqkXHl2sTuxdY2rjHDOAxhRK2G2K4'),
});
const token = localStorage.getItem('token');
await fetch('https://bot.kediritechnopark.com/webhook/subscribe', {
method: 'POST',
body: JSON.stringify({
subscription, // ← push subscription object
}),
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
});
};
function urlBase64ToUint8Array(base64String) {
const padding = '='.repeat((4 - base64String.length % 4) % 4);
const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/');
const rawData = atob(base64);
return Uint8Array.from([...rawData].map(char => char.charCodeAt(0)));
}
const openConversationsModal = () => { const openConversationsModal = () => {
setModalContent(<Conversations conversations={conversations} />); setModalContent(<Conversations conversations={conversations} />);
}; };
@@ -147,13 +188,13 @@ const Dashboard = () => {
const prefixLabelMap = { const prefixLabelMap = {
WEB: 'Web App', WEB: 'Web App',
WAP: 'WhatsApp', TGG: 'Telegram',
DME: 'Instagram', DME: 'Instagram',
}; };
const prefixColors = { const prefixColors = {
WEB: { border: '#4285F4', background: 'rgba(66, 133, 244, 0.2)' }, WEB: { border: '#4285F4', background: 'rgba(66, 133, 244, 0.2)' },
WAP: { border: '#25D366', background: 'rgba(37, 211, 102, 0.2)' }, TGG: { border: '#25D366', background: 'rgba(37, 211, 102, 0.2)' },
DME: { border: '#AA00FF', background: 'rgba(170, 0, 255, 0.2)' }, DME: { border: '#AA00FF', background: 'rgba(170, 0, 255, 0.2)' },
}; };
@@ -225,18 +266,17 @@ const Dashboard = () => {
<div className={styles.dashboardHeader}> <div className={styles.dashboardHeader}>
{isLoggedIn ? ( {isLoggedIn ? (
<div className={styles.dropdownContainer}> <div className={styles.dropdownContainer} ref={menuRef}> {/* ✅ Pindahkan ref ke sini */}
<button <button
onClick={() => setIsMenuOpen(!isMenuOpen)} onClick={() => setIsMenuOpen(!isMenuOpen)}
className={styles.dropdownToggle} className={styles.dropdownToggle}
ref={menuRef}
> >
Menu Menu
</button> </button>
{isMenuOpen && ( {isMenuOpen && (
<div className={styles.dropdownMenu}> <div className={styles.dropdownMenu}>
<button onClick={handleLogout} className={styles.dropdownItem}> <button onClick={() => navigate('/reset-password')} className={styles.dropdownItem}>
Ganti Password Ganti Password
</button> </button>
<button onClick={handleLogout} className={styles.dropdownItem}> <button onClick={handleLogout} className={styles.dropdownItem}>
@@ -245,6 +285,7 @@ const Dashboard = () => {
</div> </div>
)} )}
</div> </div>
) : ( ) : (
<a href="/login" className={styles.loginButton}>Login</a> <a href="/login" className={styles.loginButton}>Login</a>
)} )}

View File

@@ -50,7 +50,7 @@
.statsGrid { .statsGrid {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 20px; gap: 20px;
margin-top: 20px; margin-top: 20px;
} }
@@ -202,8 +202,8 @@ position: absolute;
} }
.dropdownToggle { .dropdownToggle {
background-color: #007bff; background-color: #ffff;
color: white; color: #255e54;
padding: 8px 12px; padding: 8px 12px;
border: none; border: none;
border-radius: 4px; border-radius: 4px;

104
src/ResetPassword.js Normal file
View File

@@ -0,0 +1,104 @@
import React, { useState } from 'react';
import { useNavigate } from 'react-router-dom'; // ⬅️ Tambahkan ini
import styles from './Login.module.css';
const ResetPassword = () => {
const navigate = useNavigate(); // ⬅️ Gunakan ini untuk navigasi
const [formData, setFormData] = useState({
oldPassword: '',
newPassword: ''
});
const [error, setError] = useState('');
const [success, setSuccess] = useState('');
const handleChange = (e) => {
setFormData({ ...formData, [e.target.name]: e.target.value });
};
const handleSubmit = async (e) => {
e.preventDefault();
setError('');
setSuccess('');
const token = localStorage.getItem('token');
if (!token) {
setError('Anda belum login. Silakan login terlebih dahulu.');
return;
}
try {
const response = await fetch('https://bot.kediritechnopark.com/webhook/reset-password', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`
},
body: JSON.stringify({
password: formData.oldPassword,
newPassword: formData.newPassword
})
});
const data = await response.json();
if (data?.success) {
setSuccess('Password berhasil diubah');
setFormData({ oldPassword: '', newPassword: '' });
} else {
setError(data?.message || 'Gagal mereset password');
}
} catch (err) {
console.error('Reset 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}>Ganti Password</h1>
<p className={styles.subtitle}>Masukkan password lama dan yang baru</p>
<form onSubmit={handleSubmit} className={styles.form}>
<input
type="password"
name="oldPassword"
placeholder="Password Lama"
value={formData.oldPassword}
onChange={handleChange}
className={styles.input}
/>
<input
type="password"
name="newPassword"
placeholder="Password Baru"
value={formData.newPassword}
onChange={handleChange}
className={styles.input}
/>
{error && <p className={styles.error}>{error}</p>}
{success && <p style={{ color: 'green', marginBottom: '10px' }}>{success}</p>}
<button type="submit" className={styles.button}>
Simpan Password Baru
</button>
</form>
{/* Tombol kembali */}
<button
onClick={() => navigate('/dashboard')}
className={styles.button}
style={{ marginTop: '12px', backgroundColor: '#777' }}
>
Kembali ke Dashboard
</button>
<div className={styles.footer}>
&copy; 2025 Kloowear AI - Admin Panel
</div>
</div>
</div>
);
};
export default ResetPassword;