diff --git a/frontend/src/components/chat/ChatInput.jsx b/frontend/src/components/chat/ChatInput.jsx
new file mode 100644
index 0000000..ce4a8e3
--- /dev/null
+++ b/frontend/src/components/chat/ChatInput.jsx
@@ -0,0 +1,52 @@
+import { Send } from 'lucide-react';
+
+/**
+ * Chat Input component with send button
+ *
+ * @param {string} value - Input value
+ * @param {function} onChange - Change handler for input
+ * @param {function} onSubmit - Submit handler for form
+ * @param {boolean} disabled - Whether input is disabled (e.g., when not connected)
+ * @param {string} placeholder - Placeholder text (default: "Write a message...")
+ * @param {string} className - Additional classes for the form
+ * @param {boolean} autoFocus - Whether to autofocus the input
+ */
+const ChatInput = ({
+ value,
+ onChange,
+ onSubmit,
+ disabled = false,
+ placeholder = 'Write a message...',
+ className = '',
+ autoFocus = false
+}) => {
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (!value.trim() || disabled) return;
+ onSubmit(e);
+ };
+
+ return (
+
+ );
+};
+
+export default ChatInput;
diff --git a/frontend/src/components/chat/ChatMessage.jsx b/frontend/src/components/chat/ChatMessage.jsx
new file mode 100644
index 0000000..0774b83
--- /dev/null
+++ b/frontend/src/components/chat/ChatMessage.jsx
@@ -0,0 +1,57 @@
+import Avatar from '../common/Avatar';
+
+/**
+ * Individual Chat Message component
+ *
+ * @param {object} message - Message object with { id, content, username, avatar, createdAt, userId/user_id }
+ * @param {boolean} isOwn - Whether this message belongs to the current user
+ * @param {function} formatTime - Optional custom time formatter (default: toLocaleTimeString)
+ */
+const ChatMessage = ({ message, isOwn, formatTime }) => {
+ const defaultFormatTime = (timestamp) => {
+ return new Date(timestamp).toLocaleTimeString('en-US', {
+ hour: '2-digit',
+ minute: '2-digit'
+ });
+ };
+
+ const timeFormatter = formatTime || defaultFormatTime;
+
+ return (
+
+
+
+
+
+
+ {message.username}
+
+
+ {timeFormatter(message.createdAt)}
+
+
+
+ {message.content}
+
+
+
+
+ );
+};
+
+export default ChatMessage;
diff --git a/frontend/src/components/chat/ChatMessageList.jsx b/frontend/src/components/chat/ChatMessageList.jsx
new file mode 100644
index 0000000..f6e1f9a
--- /dev/null
+++ b/frontend/src/components/chat/ChatMessageList.jsx
@@ -0,0 +1,67 @@
+import { Loader2 } from 'lucide-react';
+import ChatMessage from './ChatMessage';
+
+/**
+ * Chat Message List component with infinite scroll support
+ *
+ * @param {Array} messages - Array of message objects
+ * @param {number} currentUserId - Current user's ID to determine message ownership
+ * @param {React.Ref} messagesEndRef - Ref for scroll-to-bottom functionality
+ * @param {React.Ref} messagesContainerRef - Ref for the scrollable container
+ * @param {boolean} loadingOlder - Whether older messages are being loaded
+ * @param {boolean} hasMore - Whether there are more messages to load
+ * @param {string} emptyText - Text to show when there are no messages
+ * @param {function} formatTime - Optional custom time formatter
+ */
+const ChatMessageList = ({
+ messages = [],
+ currentUserId,
+ messagesEndRef,
+ messagesContainerRef,
+ loadingOlder = false,
+ hasMore = false,
+ emptyText = 'No messages yet. Start the conversation!',
+ formatTime
+}) => {
+ return (
+
+ {/* Loading indicator for older messages */}
+ {loadingOlder && (
+
+
+
+ )}
+
+ {/* Empty state */}
+ {messages.length === 0 && !loadingOlder && (
+
+ {emptyText}
+
+ )}
+
+ {/* Messages */}
+ {messages.map((message) => {
+ // Support both userId and user_id (database field names may vary)
+ const messageUserId = message.userId ?? message.user_id;
+ const isOwnMessage = messageUserId === currentUserId;
+
+ return (
+
+ );
+ })}
+
+ {/* Scroll anchor */}
+
+
+ );
+};
+
+export default ChatMessageList;
diff --git a/frontend/src/components/common/Alert.jsx b/frontend/src/components/common/Alert.jsx
new file mode 100644
index 0000000..3e0e794
--- /dev/null
+++ b/frontend/src/components/common/Alert.jsx
@@ -0,0 +1,63 @@
+import { CheckCircle, AlertCircle, AlertTriangle, Info } from 'lucide-react';
+
+/**
+ * Reusable Alert component for displaying success, error, warning, and info messages
+ *
+ * @param {string} type - Alert type: 'success', 'error', 'warning', 'info'
+ * @param {string} message - Message to display (if empty, component returns null)
+ * @param {React.Component} icon - Optional custom icon (overrides default)
+ * @param {function} onClose - Optional close handler for dismissible alerts
+ */
+const Alert = ({ type = 'info', message, icon: CustomIcon, onClose }) => {
+ if (!message) return null;
+
+ const styles = {
+ success: {
+ bg: 'bg-green-50',
+ border: 'border-green-200',
+ text: 'text-green-600',
+ iconComponent: CheckCircle
+ },
+ error: {
+ bg: 'bg-red-50',
+ border: 'border-red-200',
+ text: 'text-red-600',
+ iconComponent: AlertCircle
+ },
+ warning: {
+ bg: 'bg-yellow-50',
+ border: 'border-yellow-200',
+ text: 'text-yellow-600',
+ iconComponent: AlertTriangle
+ },
+ info: {
+ bg: 'bg-blue-50',
+ border: 'border-blue-200',
+ text: 'text-blue-600',
+ iconComponent: Info
+ }
+ };
+
+ const style = styles[type] || styles.info;
+ const IconComponent = CustomIcon || style.iconComponent;
+
+ return (
+
+
+
{message}
+ {onClose && (
+
+ )}
+
+ );
+};
+
+export default Alert;
diff --git a/frontend/src/components/common/FormInput.jsx b/frontend/src/components/common/FormInput.jsx
new file mode 100644
index 0000000..92eef39
--- /dev/null
+++ b/frontend/src/components/common/FormInput.jsx
@@ -0,0 +1,71 @@
+/**
+ * Reusable Form Input component with optional icon
+ *
+ * @param {string} label - Input label text
+ * @param {string} name - Input name attribute
+ * @param {string} type - Input type (text, email, password, url, number, etc.)
+ * @param {string} value - Input value
+ * @param {function} onChange - Change handler
+ * @param {React.Component} icon - Optional Lucide icon component (e.g., Mail, Lock)
+ * @param {string} placeholder - Placeholder text
+ * @param {boolean} required - Whether field is required
+ * @param {string} helperText - Optional helper text below input
+ * @param {number} maxLength - Maximum input length
+ * @param {string} className - Additional CSS classes for the container
+ * @param {string} error - Error message (shows red border and error text)
+ */
+const FormInput = ({
+ label,
+ name,
+ type = 'text',
+ value,
+ onChange,
+ icon: Icon,
+ placeholder,
+ required = false,
+ helperText,
+ maxLength,
+ className = '',
+ error,
+ ...props
+}) => {
+ return (
+
+ {label && (
+
+ )}
+
+ {Icon && (
+
+
+
+ )}
+
+
+ {error && (
+
{error}
+ )}
+ {!error && helperText && (
+
{helperText}
+ )}
+
+ );
+};
+
+export default FormInput;
diff --git a/frontend/src/components/common/FormSelect.jsx b/frontend/src/components/common/FormSelect.jsx
new file mode 100644
index 0000000..1e5002d
--- /dev/null
+++ b/frontend/src/components/common/FormSelect.jsx
@@ -0,0 +1,79 @@
+/**
+ * Reusable Form Select (dropdown) component with optional icon
+ *
+ * @param {string} label - Select label text
+ * @param {string} name - Select name attribute
+ * @param {string} value - Selected value
+ * @param {function} onChange - Change handler
+ * @param {Array} options - Array of options (strings or {value, label} objects)
+ * @param {React.Component} icon - Optional Lucide icon component (e.g., Globe, MapPin)
+ * @param {string} placeholder - Placeholder option text (if provided, adds empty option)
+ * @param {boolean} required - Whether field is required
+ * @param {string} helperText - Optional helper text below select
+ * @param {string} className - Additional CSS classes for the container
+ * @param {string} error - Error message (shows red border and error text)
+ */
+const FormSelect = ({
+ label,
+ name,
+ value,
+ onChange,
+ options = [],
+ icon: Icon,
+ placeholder,
+ required = false,
+ helperText,
+ className = '',
+ error,
+ ...props
+}) => {
+ return (
+
+ {label && (
+
+ )}
+
+ {Icon && (
+
+
+
+ )}
+
+
+ {error && (
+
{error}
+ )}
+ {!error && helperText && (
+
{helperText}
+ )}
+
+ );
+};
+
+export default FormSelect;
diff --git a/frontend/src/components/common/LoadingButton.jsx b/frontend/src/components/common/LoadingButton.jsx
new file mode 100644
index 0000000..dae9e5b
--- /dev/null
+++ b/frontend/src/components/common/LoadingButton.jsx
@@ -0,0 +1,37 @@
+import { Loader2 } from 'lucide-react';
+
+/**
+ * Reusable Button component with built-in loading state
+ *
+ * @param {boolean} loading - Shows loading spinner when true
+ * @param {React.ReactNode} children - Button content when not loading
+ * @param {string} loadingText - Text to show when loading (default: "Loading...")
+ * @param {string} className - Additional CSS classes
+ * @param {object} ...props - All other button props (onClick, disabled, etc.)
+ */
+const LoadingButton = ({
+ loading = false,
+ children,
+ loadingText = 'Loading...',
+ className = '',
+ ...props
+}) => {
+ return (
+
+ );
+};
+
+export default LoadingButton;
diff --git a/frontend/src/components/common/LoadingSpinner.jsx b/frontend/src/components/common/LoadingSpinner.jsx
new file mode 100644
index 0000000..8339586
--- /dev/null
+++ b/frontend/src/components/common/LoadingSpinner.jsx
@@ -0,0 +1,36 @@
+import { Loader2 } from 'lucide-react';
+
+/**
+ * Reusable Loading Spinner component
+ *
+ * @param {string} size - Spinner size: 'sm', 'md', 'lg' (default: 'md')
+ * @param {string} text - Optional text to display below spinner
+ * @param {boolean} fullPage - If true, centers spinner in a full-page container
+ * @param {string} className - Additional CSS classes
+ */
+const LoadingSpinner = ({ size = 'md', text, fullPage = false, className = '' }) => {
+ const sizes = {
+ sm: 'w-4 h-4',
+ md: 'w-8 h-8',
+ lg: 'w-12 h-12'
+ };
+
+ const spinner = (
+
+ );
+
+ if (fullPage) {
+ return (
+
+ {spinner}
+
+ );
+ }
+
+ return spinner;
+};
+
+export default LoadingSpinner;
diff --git a/frontend/src/components/events/EventCard.jsx b/frontend/src/components/events/EventCard.jsx
new file mode 100644
index 0000000..c022685
--- /dev/null
+++ b/frontend/src/components/events/EventCard.jsx
@@ -0,0 +1,95 @@
+import { useNavigate } from 'react-router-dom';
+import { Calendar, MapPin, Users, CheckCircle, QrCode } from 'lucide-react';
+
+/**
+ * Event Card component for displaying event information
+ *
+ * @param {object} event - Event object with { slug, name, location, startDate, endDate, participantsCount, description, isJoined }
+ * @param {number} delay - Animation delay in milliseconds
+ * @param {boolean} isNew - Whether this is a newly added card (triggers animation)
+ * @param {boolean} showCheckin - Whether to show check-in requirement message
+ */
+const EventCard = ({ event, delay = 0, isNew = false, showCheckin = false }) => {
+ const navigate = useNavigate();
+
+ const handleJoinEvent = () => navigate(`/events/${event.slug}/chat`);
+ const handleViewDetails = () => navigate(`/events/${event.slug}/details`);
+
+ return (
+
+ {/* Header */}
+
+
{event.name}
+ {event.isJoined && (
+
+
+ Joined
+
+ )}
+
+
+ {/* Event Details */}
+
+
+
+ {event.location}
+
+
+
+
+ {new Date(event.startDate).toLocaleDateString('en-US')} -{' '}
+ {new Date(event.endDate).toLocaleDateString('en-US')}
+
+
+
+
+ {event.participantsCount} participants
+
+
+
+ {/* Description */}
+ {event.description && (
+
{event.description}
+ )}
+
+ {/* Actions */}
+
+ {event.isJoined ? (
+
+ ) : showCheckin ? (
+
+
+
+ Check-in required
+
+
+ Scan the QR code at the event venue to join the chat
+
+
+ ) : null}
+
+ {/* Development mode: Show details link */}
+ {import.meta.env.DEV && (
+
+ )}
+
+
+ );
+};
+
+export default EventCard;
diff --git a/frontend/src/components/modals/ConfirmationModal.jsx b/frontend/src/components/modals/ConfirmationModal.jsx
new file mode 100644
index 0000000..eaf8512
--- /dev/null
+++ b/frontend/src/components/modals/ConfirmationModal.jsx
@@ -0,0 +1,93 @@
+import { AlertTriangle, Loader2 } from 'lucide-react';
+
+/**
+ * Reusable Confirmation Modal for actions that need user confirmation
+ *
+ * @param {boolean} isOpen - Controls modal visibility
+ * @param {function} onClose - Close/Cancel handler
+ * @param {function} onConfirm - Confirm action handler
+ * @param {string} title - Modal title
+ * @param {string} description - Optional description text
+ * @param {string} message - Main message/question to display
+ * @param {string} confirmText - Confirm button text (default: 'Confirm')
+ * @param {string} cancelText - Cancel button text (default: 'Cancel')
+ * @param {string} confirmButtonClass - Custom classes for confirm button (default: red/danger style)
+ * @param {React.Component} icon - Icon component (default: AlertTriangle)
+ * @param {string} iconBgClass - Icon background class (default: bg-red-100)
+ * @param {string} iconColorClass - Icon color class (default: text-red-600)
+ * @param {boolean} isLoading - Shows loading state on confirm button
+ * @param {string} loadingText - Text to show when loading (default: 'Loading...')
+ */
+const ConfirmationModal = ({
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+ description,
+ message,
+ confirmText = 'Confirm',
+ cancelText = 'Cancel',
+ confirmButtonClass = 'bg-red-600 hover:bg-red-700',
+ icon: Icon = AlertTriangle,
+ iconBgClass = 'bg-red-100',
+ iconColorClass = 'text-red-600',
+ isLoading = false,
+ loadingText = 'Loading...'
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()}
+ >
+ {/* Header with Icon */}
+
+
+
+
+
+
{title}
+ {description &&
{description}
}
+
+
+
+ {/* Message */}
+ {message && (
+
{message}
+ )}
+
+ {/* Actions */}
+
+
+
+
+
+
+ );
+};
+
+export default ConfirmationModal;
diff --git a/frontend/src/components/modals/Modal.jsx b/frontend/src/components/modals/Modal.jsx
new file mode 100644
index 0000000..8550728
--- /dev/null
+++ b/frontend/src/components/modals/Modal.jsx
@@ -0,0 +1,54 @@
+import { X } from 'lucide-react';
+
+/**
+ * Generic Modal component with customizable content
+ *
+ * @param {boolean} isOpen - Controls modal visibility
+ * @param {function} onClose - Close handler
+ * @param {string} title - Optional modal title
+ * @param {React.ReactNode} children - Modal content
+ * @param {string} maxWidth - Maximum width class (default: 'max-w-4xl')
+ * @param {boolean} showCloseButton - Show X button in header (default: true)
+ */
+const Modal = ({
+ isOpen,
+ onClose,
+ title,
+ children,
+ maxWidth = 'max-w-4xl',
+ showCloseButton = true
+}) => {
+ if (!isOpen) return null;
+
+ return (
+
+
e.stopPropagation()} // Prevent closing when clicking inside modal
+ >
+ {title && (
+
+
{title}
+ {showCloseButton && (
+
+ )}
+
+ )}
+
+ {children}
+
+
+
+ );
+};
+
+export default Modal;