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:
10
adventure-rental-app/package-lock.json
generated
10
adventure-rental-app/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
121
adventure-rental-app/src/ChatbotPage.jsx
Normal file
121
adventure-rental-app/src/ChatbotPage.jsx
Normal 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;
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
Reference in New Issue
Block a user