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:
@@ -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() {
|
||||
return (
|
||||
<AuthPage />
|
||||
)
|
||||
<Router>
|
||||
<Routes>
|
||||
<Route path="/admin" element={<AuthPage />} />
|
||||
<Route path="/" element={<ChatbotPage />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
.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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user