diff --git a/Postcss.config.js b/Postcss.config.js new file mode 100644 index 0000000..8567b4c --- /dev/null +++ b/Postcss.config.js @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; \ No newline at end of file diff --git a/Tailwind.config.js b/Tailwind.config.js index d0d2125..690decd 100644 --- a/Tailwind.config.js +++ b/Tailwind.config.js @@ -1,100 +1,125 @@ /** @type {import('tailwindcss').Config} */ module.exports = { - content: [ - "./index.html", - "./src/**/*.{js,jsx,ts,tsx}", - ], + content: ["./index.html", "./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: { fontFamily: { - display: ["'Syne'", "sans-serif"], - body: ["'DM Sans'", "sans-serif"], + display: ["'Plus Jakarta Sans'", "sans-serif"], + body: ["'Inter'", "sans-serif"], }, colors: { bg: { - 0: "#0a0a0f", - 1: "#111118", - 2: "#18181f", - 3: "#202028", - 4: "#2a2a35", - 5: "#343440", + 0: "#08080e", + 1: "#0d0d17", + 2: "#111120", + 3: "#161626", + 4: "#1c1c2e", + 5: "#232338", + 6: "#2a2a42", + }, + surface: { + 1: "rgba(255,255,255,0.03)", + 2: "rgba(255,255,255,0.05)", + 3: "rgba(255,255,255,0.08)", }, accent: { - DEFAULT: "#7c6dfa", - 2: "#fa6d9a", - dim: "#7c6dfa29", // 16% opacity - glow: "#7c6dfa4d", // 30% opacity + DEFAULT: "#8b5cf6", + light: "#a78bfa", + dark: "#6d28d9", + pink: "#ec4899", + glow: "rgba(139,92,246,0.35)", + muted: "rgba(139,92,246,0.12)", }, border: { - DEFAULT: "#ffffff12", // 7% white - bright: "#ffffff24", // 14% white - hover: "#ffffff38", // 22% white + DEFAULT: "rgba(255,255,255,0.06)", + bright: "rgba(255,255,255,0.10)", + strong: "rgba(255,255,255,0.15)", + accent: "rgba(139,92,246,0.30)", }, text: { - 0: "#f0f0f8", - 1: "#a8a8c0", - 2: "#6a6a88", - 3: "#3e3e55", + 0: "#eeeef5", + 1: "#9898b0", + 2: "#5a5a75", + 3: "#333348", }, - online: "#3ddc97", + online: "#34d399", + danger: "#f87171", + warning: "#fbbf24", }, animation: { - "pulse-dot": "pulseDot 2s ease-in-out infinite", + "pulse-dot": "pulseDot 2.5s ease-in-out infinite", "rotate-ring": "rotateRing 3s linear infinite", - "bubble-in": "bubbleIn 0.22s cubic-bezier(0.34,1.56,0.64,1) both", - "fade-in": "fadeIn 0.25s ease both", - "scale-in": "scaleIn 0.2s cubic-bezier(0.34,1.56,0.64,1) both", - "slide-left": "slideLeft 0.2s ease both", - "typing-1": "typingBounce 1.2s ease-in-out 0s infinite", - "typing-2": "typingBounce 1.2s ease-in-out 0.2s infinite", - "typing-3": "typingBounce 1.2s ease-in-out 0.4s infinite", - "spin-slow": "spin 0.8s linear infinite", + "bubble-in": "bubbleIn 0.28s cubic-bezier(0.34,1.56,0.64,1) both", + "fade-up": "fadeUp 0.3s ease both", + "fade-in": "fadeIn 0.2s ease both", + "scale-in": "scaleIn 0.25s cubic-bezier(0.34,1.56,0.64,1) both", + "slide-right": "slideRight 0.25s ease both", + "typing-1": "typingBounce 1.4s ease-in-out 0s infinite", + "typing-2": "typingBounce 1.4s ease-in-out 0.18s infinite", + "typing-3": "typingBounce 1.4s ease-in-out 0.36s infinite", + "spin-slow": "spin 0.75s linear infinite", + "shimmer": "shimmer 2s linear infinite", }, keyframes: { pulseDot: { - "0%,100%": { boxShadow: "0 0 6px #7c6dfa" }, - "50%": { boxShadow: "0 0 18px #7c6dfa, 0 0 32px #7c6dfa4d", transform: "scale(1.25)" }, + "0%,100%": { opacity: "1", transform: "scale(1)" }, + "50%": { opacity: "0.6", transform: "scale(0.8)" }, }, rotateRing: { from: { transform: "rotate(0deg)" }, to: { transform: "rotate(360deg)" }, }, bubbleIn: { - from: { opacity: "0", transform: "scale(0.85) translateY(8px)" }, + from: { opacity: "0", transform: "scale(0.88) translateY(10px)" }, to: { opacity: "1", transform: "scale(1) translateY(0)" }, }, - fadeIn: { - from: { opacity: "0", transform: "translateY(8px)" }, + fadeUp: { + from: { opacity: "0", transform: "translateY(14px)" }, to: { opacity: "1", transform: "translateY(0)" }, }, + fadeIn: { + from: { opacity: "0" }, + to: { opacity: "1" }, + }, scaleIn: { - from: { opacity: "0", transform: "scale(0.92)" }, + from: { opacity: "0", transform: "scale(0.90)" }, to: { opacity: "1", transform: "scale(1)" }, }, - slideLeft: { - from: { opacity: "0", transform: "translateX(-12px)" }, + slideRight: { + from: { opacity: "0", transform: "translateX(-16px)" }, to: { opacity: "1", transform: "translateX(0)" }, }, typingBounce: { - "0%,60%,100%": { transform: "translateY(0)", opacity: "0.4" }, - "30%": { transform: "translateY(-6px)", opacity: "1" }, + "0%,60%,100%": { transform: "translateY(0)", opacity: "0.35" }, + "30%": { transform: "translateY(-5px)", opacity: "1" }, + }, + shimmer: { + "0%": { backgroundPosition: "-200% center" }, + "100%": { backgroundPosition: "200% center" }, }, }, boxShadow: { - glow: "0 4px 16px #7c6dfa4d", - "glow-lg": "0 6px 24px #7c6dfa66", - dark: "0 8px 32px rgba(0,0,0,0.5)", - darker: "0 20px 60px rgba(0,0,0,0.7)", + "accent": "0 4px 20px rgba(139,92,246,0.35)", + "accent-lg": "0 8px 32px rgba(139,92,246,0.45)", + "card": "0 4px 24px rgba(0,0,0,0.5), 0 1px 0 rgba(255,255,255,0.06) inset", + "card-lg": "0 16px 48px rgba(0,0,0,0.6), 0 1px 0 rgba(255,255,255,0.07) inset", + "glow-sm": "0 0 20px rgba(139,92,246,0.3)", + "inset-top": "inset 0 1px 0 rgba(255,255,255,0.08)", }, backgroundImage: { - "bubble-mine": "linear-gradient(135deg, #7c6dfa, #9b8dff)", - "ring-grad": "linear-gradient(135deg, #7c6dfa, #fa6d9a)", - "auth-glow1": "radial-gradient(circle, rgba(124,109,250,0.10), transparent 70%)", - "auth-glow2": "radial-gradient(circle, rgba(250,109,154,0.07), transparent 70%)", + "grad-accent": "linear-gradient(135deg, #6d28d9, #8b5cf6, #a78bfa)", + "grad-pink": "linear-gradient(135deg, #8b5cf6, #ec4899)", + "grad-mesh": "radial-gradient(ellipse 80% 50% at 50% -20%, rgba(139,92,246,0.15), transparent)", + "noise": "url(\"data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.03'/%3E%3C/svg%3E\")", }, width: { sidebar: "300px" }, minWidth: { sidebar: "300px" }, - maxWidth: { auth: "440px" }, + maxWidth: { auth: "420px" }, + borderRadius: { + "2xl": "16px", + "3xl": "20px", + "4xl": "24px", + }, }, }, plugins: [], diff --git a/index.html b/index.html index cc99a71..c8efc42 100644 --- a/index.html +++ b/index.html @@ -1,12 +1,19 @@ - + + + Pulse Chat +
+ - + \ No newline at end of file diff --git a/src/components/auth/Authpage.jsx b/src/components/auth/Authpage.jsx index 87671b3..0a64056 100644 --- a/src/components/auth/Authpage.jsx +++ b/src/components/auth/Authpage.jsx @@ -1,10 +1,9 @@ // src/components/auth/AuthPage.jsx import React, { useState, useCallback } from 'react'; -import { useAuth } from '../../context/AuthContext'; +import { useAuth } from '../../context/AuthContext'; import { Button, Input, Divider } from '../ui'; import { EmailIcon, LockIcon, UserIcon } from '../ui/Icons'; -/* ── Validation ──────────────────────────────────────────── */ function validateLogin({ email, password }) { const e = {}; if (!email || !email.includes('@')) e.email = 'Enter a valid email.'; @@ -14,30 +13,26 @@ function validateLogin({ email, password }) { function validateRegister({ username, email, password, confirmPassword }) { const e = {}; - if (!username || username.trim().length < 2) e.username = 'At least 2 characters.'; - if (!email || !email.includes('@')) e.email = 'Enter a valid email.'; - if (!password || password.length < 6) e.password = 'At least 6 characters.'; - if (password !== confirmPassword) e.confirmPassword = 'Passwords do not match.'; + if (!username || username.trim().length < 2) e.username = 'At least 2 characters.'; + if (!email || !email.includes('@')) e.email = 'Enter a valid email.'; + if (!password || password.length < 6) e.password = 'At least 6 characters.'; + if (password !== confirmPassword) e.confirmPassword = 'Passwords do not match.'; return e; } -/* ── Login Form ──────────────────────────────────────────── */ function LoginForm({ onSwitch }) { const { login } = useAuth(); const [f, setF] = useState({ email: '', password: '' }); const [errs, setErrs] = useState({}); const [apiErr, setApiErr] = useState(''); const [loading, setLoad] = useState(false); + const set = k => e => setF(p => ({ ...p, [k]: e.target.value })); - const set = (k) => (e) => setF(p => ({ ...p, [k]: e.target.value })); - - const submit = useCallback(async (e) => { - e.preventDefault(); - setApiErr(''); + const submit = useCallback(async e => { + e.preventDefault(); setApiErr(''); const errs = validateLogin(f); if (Object.keys(errs).length) { setErrs(errs); return; } - setErrs({}); - setLoad(true); + setErrs({}); setLoad(true); try { await login(f); } catch (err) { setApiErr(err.message); } finally { setLoad(false); } @@ -47,47 +42,36 @@ function LoginForm({ onSwitch }) {
} error={errs.email} autoComplete="email" /> } error={errs.password} autoComplete="current-password" /> - - {apiErr && ( -
- {apiErr} -
- )} - - + {apiErr &&
{apiErr}
} + -

- Don't have an account?{' '} - + No account?{' '} +

- -
- Demo: demo@pulse.chat / demo1234 +
+
+ +
+

Demo: demo@pulse.chat · demo1234

); } -/* ── Register Form ───────────────────────────────────────── */ function RegisterForm({ onSwitch }) { const { register } = useAuth(); const [f, setF] = useState({ username: '', email: '', password: '', confirmPassword: '' }); const [errs, setErrs] = useState({}); const [apiErr, setApiErr] = useState(''); const [loading, setLoad] = useState(false); + const set = k => e => setF(p => ({ ...p, [k]: e.target.value })); - const set = (k) => (e) => setF(p => ({ ...p, [k]: e.target.value })); - - const submit = useCallback(async (e) => { - e.preventDefault(); - setApiErr(''); + const submit = useCallback(async e => { + e.preventDefault(); setApiErr(''); const errs = validateRegister(f); if (Object.keys(errs).length) { setErrs(errs); return; } - setErrs({}); - setLoad(true); + setErrs({}); setLoad(true); try { await register(f); } catch (err) { setApiErr(err.message); } finally { setLoad(false); } @@ -95,79 +79,85 @@ function RegisterForm({ onSwitch }) { return (
- } error={errs.username} autoComplete="username" /> + } error={errs.username} autoComplete="username" /> } error={errs.email} autoComplete="email" /> - } error={errs.password} autoComplete="new-password" /> + } error={errs.password} autoComplete="new-password" /> } error={errs.confirmPassword} autoComplete="new-password" /> - - {apiErr && ( -
- {apiErr} -
- )} - - + {apiErr &&
{apiErr}
} + -

Already have an account?{' '} - +

); } -/* ── AuthPage ────────────────────────────────────────────── */ export default function AuthPage() { const [mode, setMode] = useState('login'); return ( -
- {/* Ambient glows */} -
-
+
+ {/* Background mesh */} +
+
+
{/* Card */} -
- {/* Logo */} -
-
- Pulse +
+ {/* Glass card */} +
+ {/* Header band */} +
+ {/* Logo */} +
+
+ + + +
+ Pulse +
+ + {/* Heading */} +
+

+ {mode === 'login' ? 'Welcome back' : 'Create account'} +

+

+ {mode === 'login' ? 'Sign in to continue to Pulse' : 'Join Pulse and start chatting'} +

+
+ + {/* Tab pills */} +
+ {['login', 'register'].map(m => ( + + ))} +
+ + {/* Form */} +
+ {mode === 'login' + ? setMode('register')} /> + : setMode('login')} /> + } +
+
- {/* Tabs */} -
- - - {/* Sliding indicator */} -
-
- - {/* Form */} -
- {mode === 'login' - ? setMode('register')} /> - : setMode('login')} /> - } -
+

+ Pulse · Secure · Private · Fast +

- -

Pulse Chat · Built with React + Tailwind

); } \ No newline at end of file diff --git a/src/components/chat/ChatList.jsx b/src/components/chat/ChatList.jsx index d81674e..a448cbf 100644 --- a/src/components/chat/ChatList.jsx +++ b/src/components/chat/ChatList.jsx @@ -6,63 +6,62 @@ import { Avatar, Badge, IconButton } from '../ui'; import { SearchIcon, EditIcon } from '../ui/Icons'; import { chatService } from '../../services/chatService'; -/* ── Online contact ring ─────────────────────────────────── */ function OnlineContact({ user, onClick }) { return ( - ); } -/* ── Conversation item ───────────────────────────────────── */ function ConvItem({ conv, isActive, currentUserId, onClick }) { - const other = conv.type === 'direct' ? conv.participants?.find(p => p?.id !== currentUserId) : null; - const name = conv.name || other?.username || 'Unknown'; - const color = other?.color || '#7c6dfa'; + const other = conv.type === 'direct' ? conv.participants?.find(p => p?.id !== currentUserId) : null; + const name = conv.name || other?.username || 'Unknown'; + const color = other?.color || '#8b5cf6'; const initials = conv.name ? conv.name[0] : (other?.avatar || '??'); - const preview = conv.lastMessage + const preview = conv.lastMessage ? (conv.lastMessage.senderId === currentUserId ? 'You: ' : '') + conv.lastMessage.text - : 'No messages yet'; + : 'Start a conversation'; + const isUnread = conv.unread > 0; return ( - - ))} +
+
Start new chat
+
+ {contacts.map(c => ( + + ))} +
)} {/* Search */} -
- - - - setSearch(e.target.value)} - /> - {search && ( - - )} +
+
+ + + + setSearch(e.target.value)} + /> + {search && ( + + )} +
{/* Online strip */} {online.length > 0 && !search && ( - <> -

Active now

-
- {online.map(u => ( - startConv(u.id)} /> - ))} +
+

Active Now

+
+ {online.map(u => startConv(u.id)} />)}
- +
)} - {/* Section label */} -

- {search ? 'Results' : 'Recent'} -

- - {/* Conversation list */} -
- {filtered.length === 0 ? ( -
- -

No conversations found

-
- ) : ( - filtered.map(conv => ( - selectConv(conv.id)} - /> - )) - )} + {/* Conversations */} + {!search &&

Recent

} + +
+ {filtered.length === 0 + ?

No results

+ : filtered.map(conv => ( + selectConv(conv.id)} /> + )) + }
{/* Footer */} -
- +
+
+ +
+
-
{user?.username}
-
- - Online -
+
{user?.username}
+
● Active
diff --git a/src/components/chat/ChatWindow.jsx b/src/components/chat/ChatWindow.jsx index bbeafb3..d23dc76 100644 --- a/src/components/chat/ChatWindow.jsx +++ b/src/components/chat/ChatWindow.jsx @@ -1,148 +1,143 @@ // src/components/chat/ChatWindow.jsx import React, { useEffect, useRef, Fragment } from 'react'; -import { useAuth } from '../../context/AuthContext'; -import { useChat } from '../../context/ChatContext'; -import { IconButton } from '../ui'; +import { useAuth } from '../../context/AuthContext'; +import { useChat } from '../../context/ChatContext'; +import { IconButton } from '../ui'; import { BackIcon, PhoneIcon, VideoIcon, InfoIcon, ChatBubbleIcon } from '../ui/Icons'; import MessageBubble from './MessageBubble'; import MessageInput from './MessageInput'; import TypingIndicator from './TypingIndicator'; -/* ── Date divider ────────────────────────────────────────── */ function DateDivider({ date }) { const d = new Date(date); const now = new Date(); const diff = Math.floor((now - d) / 86400000); const label = diff === 0 ? 'Today' : diff === 1 ? 'Yesterday' - : d.toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }); + : d.toLocaleDateString(undefined, { weekday: 'long', month: 'short', day: 'numeric' }); return ( -
+
- {label} + {label}
); } -/* ── Empty state ─────────────────────────────────────────── */ function EmptyState() { return ( -
-
- +
+ {/* Decorative icon */} +
+
+ +
+
+
+
+

No chat selected

+

+ Pick a conversation from the sidebar to start messaging +

+
+
+
+ 4 contacts online
-

Your messages

-

- Select a conversation or start a new one to begin chatting. -

); } -/* ── ChatWindow ──────────────────────────────────────────── */ export default function ChatWindow() { - const { user } = useAuth(); + const { user } = useAuth(); const { activeConv, - activeConvId: activeId, + activeConvId: activeId, activeMessages: activeMsgs, activeTyping, contacts, sendMessage, toggleSidebar: toggleSb, - } = useChat(); + } = useChat(); - const bottomRef = useRef(null); + const bottomRef = useRef(null); - // Auto-scroll on new messages useEffect(() => { bottomRef.current?.scrollIntoView({ behavior: 'smooth' }); }, [activeMsgs, activeTyping]); if (!activeId || !activeConv) { return ( -
+
); } - // Resolve other participant - const other = activeConv.type === 'direct' - ? activeConv.participants?.find(p => p?.id !== user?.id) - : null; + const other = activeConv.type === 'direct' ? activeConv.participants?.find(p => p?.id !== user?.id) : null; + const displayName = activeConv.name || other?.username || 'Unknown'; + const color = other?.color || '#8b5cf6'; + const initials = activeConv.name ? activeConv.name[0] : (other?.avatar || '??'); + const isOnline = other?.online ?? false; - const displayName = activeConv.name || other?.username || 'Unknown'; - const headerColor = other?.color || '#7c6dfa'; - const headerInitials = activeConv.name ? activeConv.name[0] : (other?.avatar || '??'); - const isOnline = other?.online ?? false; - - // Helper: resolve a user object by ID for bubbles - const resolveUser = (senderId) => { - if (senderId === user?.id) return user; - return contacts?.find(c => c.id === senderId) || { id: senderId, avatar: '?', color: '#888' }; + const resolveUser = id => { + if (id === user?.id) return user; + return contacts?.find(c => c.id === id) || { id, avatar: '?', color: '#8b5cf6' }; }; - // Group into runs: annotate first / last in each sender run + date breaks const grouped = activeMsgs.map((msg, i) => { - const prev = activeMsgs[i - 1]; - const next = activeMsgs[i + 1]; - const isFirst = !prev || prev.senderId !== msg.senderId; - const isLast = !next || next.senderId !== msg.senderId; - const showDate = !prev || new Date(prev.time).toDateString() !== new Date(msg.time).toDateString(); + const prev = activeMsgs[i - 1]; + const next = activeMsgs[i + 1]; + const isFirst = !prev || prev.senderId !== msg.senderId; + const isLast = !next || next.senderId !== msg.senderId; + const showDate = !prev || new Date(prev.time).toDateString() !== new Date(msg.time).toDateString(); return { msg, isFirst, isLast, showDate }; }); return ( -
- - {/* ── Header ──────────────────────────────────────────── */} -
- {/* Back (mobile) */} +
+ {/* ── Chat Header ─────────────────────────────────────── */} +
{/* Avatar */}
-
- {headerInitials} +
+ {initials}
- {isOnline && ( -
- )} + {isOnline &&
}
- {/* Name + status */} + {/* Info */}
-
{displayName}
-
- {isOnline ? ( - <> - - Active now - - ) : 'Offline'} +
{displayName}
+
+ {isOnline + ? <>Active now + : Offline + }
- {/* Action buttons */} -
+ {/* Actions */} +
- +
- {/* ── Message list ────────────────────────────────────── */} -
+ {/* ── Messages ────────────────────────────────────────── */} +
{activeMsgs.length === 0 && ( -
- No messages yet. Say hello! 👋 +
+
+ 👋 +
+

Say hello to {displayName}!

)} @@ -164,10 +159,7 @@ export default function ChatWindow() {
{/* ── Input ───────────────────────────────────────────── */} - +
); } \ No newline at end of file diff --git a/src/components/chat/MessageBubble.jsx b/src/components/chat/MessageBubble.jsx index 2fa5f58..f8c0d94 100644 --- a/src/components/chat/MessageBubble.jsx +++ b/src/components/chat/MessageBubble.jsx @@ -1,61 +1,62 @@ // src/components/chat/MessageBubble.jsx import React from 'react'; import { chatService, MESSAGE_STATUS } from '../../services/chatService'; -import { CheckIcon, CheckCheckIcon } from '../ui/Icons'; +import { CheckIcon, CheckCheckIcon } from '../ui/Icons'; function StatusTick({ status }) { if (status === MESSAGE_STATUS.SENDING) return ( - - - + + + ); - if (status === MESSAGE_STATUS.SENT) return ( - - ); - if (status === MESSAGE_STATUS.DELIVERED) return ( - - ); - if (status === MESSAGE_STATUS.READ) return ( - - ); + if (status === MESSAGE_STATUS.SENT) return ; + if (status === MESSAGE_STATUS.DELIVERED) return ; + if (status === MESSAGE_STATUS.READ) return ; return null; } export default function MessageBubble({ message, isMine, isFirst, isLast, senderUser }) { return ( -
- {/* Avatar slot */} -
+
+ {/* Avatar slot — always reserve space for alignment */} +
{!isMine && isLast && (
{senderUser?.avatar || '?'}
)}
- {/* Bubble + meta */} -
+ {/* Bubble + timestamp */} +
{!isMine && isFirst && senderUser && ( - {senderUser.username} + {senderUser.username} )}
{message.text}
{isLast && ( -
+
{chatService.fmtMsgTime(message.time)} {isMine && }
diff --git a/src/components/chat/MessageInput.jsx b/src/components/chat/MessageInput.jsx index 7eaa700..3c1c984 100644 --- a/src/components/chat/MessageInput.jsx +++ b/src/components/chat/MessageInput.jsx @@ -2,121 +2,87 @@ import React, { useState, useRef, useCallback } from 'react'; import { EmojiIcon, AttachIcon, SendIcon } from '../ui/Icons'; -const EMOJIS = ['😊','🎉','👍','🔥','✨','💡','🙌','👀','😄','🚀','❤️','😂','💯','🤔','🥳']; +const EMOJIS = ['😊','🎉','👍','🔥','✨','💡','🙌','👀','😄','🚀','❤️','😂','💯','🤔','🥳','⚡','🎯','💎']; export default function MessageInput({ onSend, placeholder = 'Type a message…' }) { - const [text, setText] = useState(''); - const [showEmoji, setShowEmoji] = useState(false); - const textareaRef = useRef(null); - const canSend = text.trim().length > 0; + const [text, setText] = useState(''); + const [showEmoji, setShowEmoji] = useState(false); + const taRef = useRef(null); + const canSend = text.trim().length > 0; const handleSend = useCallback(() => { if (!canSend) return; - onSend(text); - setText(''); - setShowEmoji(false); - if (textareaRef.current) textareaRef.current.style.height = 'auto'; - textareaRef.current?.focus(); + onSend(text); setText(''); setShowEmoji(false); + if (taRef.current) taRef.current.style.height = 'auto'; + taRef.current?.focus(); }, [text, canSend, onSend]); - const handleKeyDown = (e) => { - if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } - }; + const onKey = e => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; - const handleInput = (e) => { + const onInput = e => { setText(e.target.value); e.target.style.height = 'auto'; e.target.style.height = Math.min(e.target.scrollHeight, 120) + 'px'; }; - const insertEmoji = (emoji) => { - setText(t => t + emoji); - setShowEmoji(false); - textareaRef.current?.focus(); - }; - return ( -
- +
{/* Emoji picker */} {showEmoji && ( -
- {EMOJIS.map(e => ( - - ))} +
+
+ {EMOJIS.map(e => ( + + ))} +
)}
- {/* Textarea wrapper */} -
-