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 = ( +
+ + {text &&

{text}

} +
+ ); + + 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;