feat(chat): implement initial chatbot UI and functionality

This commit introduces the foundational user-facing chatbot interface.

- Adds a new `ChatbotPage` component with a complete UI for displaying messages, user input, and a simulated bot response.
- Implements a "typing" indicator for a better user experience.
- Integrates `react-router-dom` to handle application routing, directing the root path to the new chatbot and moving the admin login to `/admin`.
- Adds `react-icons` as a new dependency for UI elements.
- Extends Tailwind CSS theme with new colors and adds custom CSS for the typing animation.
This commit is contained in:
Emmanuel Rizky
2025-08-03 13:03:32 +07:00
parent b32cf4c374
commit ecc5ab4898
6 changed files with 177 additions and 3 deletions

View File

@@ -11,6 +11,7 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.1" "react-router-dom": "^7.7.1"
}, },
"devDependencies": { "devDependencies": {
@@ -3580,6 +3581,15 @@
"react": "^19.1.1" "react": "^19.1.1"
} }
}, },
"node_modules/react-icons": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.5.0.tgz",
"integrity": "sha512-MEFcXdkP3dLo8uumGI5xN3lDFNsRtrjbOEKDLD7yv76v4wpnEq2Lt2qeHaQOr34I/wPN3s3+N08WkQ+CW37Xiw==",
"license": "MIT",
"peerDependencies": {
"react": "*"
}
},
"node_modules/react-refresh": { "node_modules/react-refresh": {
"version": "0.17.0", "version": "0.17.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz",

View File

@@ -13,6 +13,7 @@
"axios": "^1.11.0", "axios": "^1.11.0",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-icons": "^5.5.0",
"react-router-dom": "^7.7.1" "react-router-dom": "^7.7.1"
}, },
"devDependencies": { "devDependencies": {

View File

@@ -1,9 +1,16 @@
import AuthPage from './AuthPage' import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import AuthPage from './AuthPage';
import ChatbotPage from './ChatbotPage';
function App() { function App() {
return ( return (
<AuthPage /> <Router>
) <Routes>
<Route path="/admin" element={<AuthPage />} />
<Route path="/" element={<ChatbotPage />} />
</Routes>
</Router>
);
} }
export default App export default App

View File

@@ -0,0 +1,121 @@
import React, { useState } from 'react';
import { FiCamera, FiSend, FiUser } from 'react-icons/fi';
const ChatbotPage = () => {
const [messages, setMessages] = useState([
{ id: 1, text: 'Halo Kak! 👋 Selamat datang di Adventure Rental. Saya Maya, asisten virtual yang siap membantu Anda. Ada yang bisa saya bantu?', sender: 'bot', timestamp: '10:30' },
]);
const [inputMessage, setInputMessage] = useState('');
const [isTyping, setIsTyping] = useState(false);
const handleSendMessage = () => {
if (inputMessage.trim() === '') return;
const newMessage = {
id: messages.length + 1,
text: inputMessage,
sender: 'user',
timestamp: new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
};
setMessages((prevMessages) => [...prevMessages, newMessage]);
setInputMessage('');
setIsTyping(true);
// Simulate bot response
setTimeout(() => {
const botResponse = {
id: messages.length + 3, // Adjusted for the new message and typing indicator
text: `Anda mengirim: "${inputMessage}"`,
sender: 'bot',
timestamp: new Date().toLocaleTimeString('en-US', { hour: '2-digit', minute: '2-digit', hour12: false }),
};
setIsTyping(false);
setMessages((prevMessages) => [...prevMessages, botResponse]);
}, 2000);
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleSendMessage();
}
};
return (
<div className="w-full h-screen flex items-center justify-center bg-login-bg bg-cover bg-center p-4">
<div className="w-full max-w-md md:max-w-2xl h-full md:h-[95vh] md:max-h-[800px] flex flex-col bg-black/30 backdrop-blur-xl rounded-2xl shadow-2xl overflow-hidden">
{/* Header */}
<div className="p-3 flex items-center space-x-3 flex-shrink-0">
<div className="w-10 h-10 rounded-full bg-brand-orange flex items-center justify-center text-white text-xl font-bold">
M
</div>
<div>
<h1 className="text-white font-bold">Maya</h1>
<p className="text-gray-200 text-xs">Adventure Assistant</p>
</div>
</div>
{/* Chat Window */}
<div className="flex-grow p-4 overflow-y-auto bg-white/20">
{messages.map((message) => (
<div key={message.id} className={`relative w-full flex ${message.sender === 'user' ? 'justify-end' : 'justify-start'} mb-6`}>
<div className={`max-w-[calc(100%-3rem)] ${message.sender === 'user' ? 'order-1' : 'order-2'}`}>
<div className={`p-3 rounded-lg shadow-md ${message.sender === 'user' ? 'bg-brand-orange text-white rounded-br-sm' : 'bg-white text-gray-800 rounded-bl-sm'}`}>
<p>{message.text}</p>
<p className={`text-xs mt-1 ${message.sender === 'user' ? 'text-gray-200 text-left' : 'text-gray-500 text-right'}`}>{message.timestamp}</p>
</div>
</div>
{message.sender === 'bot' && (
<div className="absolute bottom-0 left-2 w-8 h-8 rounded-full bg-brand-orange flex items-center justify-center text-white font-bold border-2 border-white/50 -mb-4">
M
</div>
)}
{message.sender === 'user' && (
<div className="absolute bottom-0 right-2 w-8 h-8 rounded-full bg-white flex items-center justify-center text-gray-600 border-2 border-white/50 -mb-4">
<FiUser size={20} />
</div>
)}
</div>
))}
{isTyping && (
<div className="relative w-full flex justify-start mb-6">
<div className="absolute bottom-0 left-2 w-8 h-8 rounded-full bg-brand-orange flex items-center justify-center text-white font-bold border-2 border-white/50 -mb-4">
M
</div>
<div className="max-w-[calc(100%-3rem)] ml-8">
<div className="p-3 rounded-lg bg-white text-gray-800 shadow-md">
<div className="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
)}
</div>
{/* Input Area */}
<div className="p-3 flex items-center space-x-3 flex-shrink-0">
<div className="flex-grow relative">
<input
type="text"
placeholder="Ketik pesan..."
className="w-full p-3 rounded-lg bg-white/20 text-white placeholder-gray-200 focus:outline-none focus:ring-2 focus:ring-brand-orange"
value={inputMessage}
onChange={(e) => setInputMessage(e.target.value)}
onKeyPress={handleKeyPress}
/>
</div>
<button
className="bg-brand-orange text-white w-12 h-12 rounded-lg flex items-center justify-center hover:bg-orange-600 transition duration-300 flex-shrink-0"
onClick={handleSendMessage}
>
<FiSend size={24} />
</button>
</div>
</div>
</div>
);
};
export default ChatbotPage;

View File

@@ -17,3 +17,35 @@ input:-webkit-autofill:active {
transition: background-color 5000s ease-in-out 0s; transition: background-color 5000s ease-in-out 0s;
} }
.typing-indicator {
display: flex;
align-items: center;
}
.typing-indicator span {
height: 8px;
width: 8px;
background-color: #9E9E9E;
border-radius: 50%;
display: inline-block;
margin: 0 2px;
animation: typing-wave 1.4s infinite ease-in-out both;
}
.typing-indicator span:nth-child(1) {
animation-delay: -0.32s;
}
.typing-indicator span:nth-child(2) {
animation-delay: -0.16s;
}
@keyframes typing-wave {
0%, 80%, 100% {
transform: scale(0);
}
40% {
transform: scale(1.0);
}
}

View File

@@ -12,6 +12,9 @@ export default {
'brand-light': '#F5F5DC', 'brand-light': '#F5F5DC',
'brand-gray': '#A9A9A9', 'brand-gray': '#A9A9A9',
'brand-orange': '#D97706', // Adventure Orange 'brand-orange': '#D97706', // Adventure Orange
'chat-header': '#003C5A',
'chat-bg': '#0084A8',
'chat-input-bg': '#0A192F',
}, },
fontFamily: { fontFamily: {
sans: ['Poppins', 'sans-serif'], sans: ['Poppins', 'sans-serif'],