feat: enhance chat room UI with user avatars and message improvements

- Added user avatar generation with external avatar service
- Implemented message styling with user-specific layouts
- Added message length counter and validation
- Updated CSS for improved message and user profile display
- Restricted message length to 2048 characters
- Added disabled state for send button based on message length
This commit is contained in:
Juan Sebastián Montoya 2025-03-07 01:06:31 -05:00
parent 3d41e2cc42
commit d4d99fb5e7
5 changed files with 144 additions and 24 deletions

View file

@ -10,9 +10,9 @@
"test": "ts-node --test test/**/*.test.ts",
"start": "node dist/index.js",
"dev": "nodemon --delay 2000ms src/index.ts",
"prisma:generate": "dotenv -e ../../.env -- prisma generate",
"prisma:migrate": "dotenv -e ../../.env -- prisma migrate dev",
"prisma:init": "dotenv -e ../../.env -- prisma migrate dev --name init",
"prisma:generate": "dotenv -e ../../.env.local -- prisma generate",
"prisma:migrate": "dotenv -e ../../.env.local -- prisma migrate dev",
"prisma:init": "dotenv -e ../../.env.local -- prisma migrate dev --name init",
"build": "tsc"
},
"keywords": [],

View file

@ -9,7 +9,7 @@ import { PrismaClient } from '@prisma/client';
import fastifyCors from '@fastify/cors';
import { z } from 'zod';
dotenv.config({ path: '../../.env' });
dotenv.config({ path: '../../.env.local' });
const { allowedOrigins, port, host } = z
.object({

View file

@ -79,6 +79,35 @@ body {
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.user-profile {
display: flex;
align-items: center;
gap: 1rem;
}
.user-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
border: 2px solid white;
}
.logout-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
background-color: rgba(255, 255, 255, 0.2);
color: white;
border: none;
border-radius: 0.25rem;
cursor: pointer;
transition: background-color 0.2s;
}
.logout-button:hover {
background-color: rgba(255, 255, 255, 0.3);
}
.app-main {
flex: 1;
padding: 1rem;
@ -180,11 +209,6 @@ button:disabled {
border-radius: 0.25rem;
}
.logout-button {
padding: 0.5rem 1rem;
font-size: 0.875rem;
}
/* Chat styles */
.chat-container {
display: flex;
@ -357,21 +381,53 @@ button:disabled {
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem;
}
.message-wrapper {
display: flex;
align-items: flex-end;
max-width: 100%;
gap: 0.75rem;
}
.message-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
object-fit: cover;
align-self: flex-end;
}
.message {
max-width: 70%;
max-width: 85%;
min-width: 100px;
padding: 0.75rem;
border-radius: 0.5rem;
word-break: break-word;
line-height: 1.4;
width: fit-content;
display: inline-block;
}
.other-message-wrapper {
align-self: flex-start;
}
.own-message-wrapper {
align-self: flex-end;
flex-direction: row-reverse;
}
.other-message {
background-color: #f3f4f6;
align-self: flex-start;
}
.message.own-message {
.own-message {
background-color: rgba(79, 70, 229, 0.1);
align-self: flex-end;
}
@ -385,18 +441,31 @@ button:disabled {
.username {
font-weight: 500;
margin-right: 0.5rem;
}
.time {
color: var(--secondary-color);
opacity: 0.7;
}
.message-content {
word-break: break-word;
white-space: pre-wrap;
display: inline-block;
text-align: justify;
}
.message-length-counter {
font-size: 0.75rem;
color: var(--secondary-color);
margin-right: 0.5rem;
align-self: center;
}
.message-form {
display: flex;
align-items: center;
padding: 1rem;
border-top: 1px solid var(--border-color);
}
@ -413,6 +482,11 @@ button:disabled {
border-radius: 0 0.25rem 0.25rem 0;
}
.message-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Create Room styles */
.create-room {
padding: 1rem;

View file

@ -14,6 +14,10 @@ function App() {
const [selectedRoomId, setSelectedRoomId] = createSignal<string | null>(null);
const [showRegister, setShowRegister] = createSignal(false);
// Function to generate avatar URL
const getUserAvatarUrl = (userId: string, size: number = 40) =>
`https://avatars.jusemon.com/${userId}?size=${size}`;
// Check if user is already authenticated
const checkAuth = () => {
const token = localStorage.getItem('token');
@ -55,9 +59,16 @@ function App() {
<header class='app-header'>
<h1>Unreal Chat</h1>
{isAuthenticated() && (
<button class='logout-button' onClick={handleLogout}>
Logout
</button>
<div class='user-profile'>
<img
src={getUserAvatarUrl(userId())}
alt='User avatar'
class='user-avatar'
/>
<button class='logout-button' onClick={handleLogout}>
Logout
</button>
</div>
)}
</header>

View file

@ -161,14 +161,18 @@ export function ChatRoom(props: ChatRoomProps) {
}
});
const MAX_MESSAGE_LENGTH = 2048;
const handleSendMessage = async (e: Event) => {
e.preventDefault();
if (!message().trim()) return;
const trimmedMessage = message().trim();
if (!trimmedMessage) return;
if (trimmedMessage.length > MAX_MESSAGE_LENGTH) return;
try {
await sendMessage({
content: message(),
content: trimmedMessage,
roomId: props.roomId,
});
setMessage('');
@ -242,6 +246,10 @@ export function ChatRoom(props: ChatRoomProps) {
};
});
// Function to generate avatar URL
const getUserAvatarUrl = (userId: string, size: number = 40) =>
`https://avatars.jusemon.com/${userId}?size=${size}`;
return (
<div class='chat-room'>
<Show when={roomQuery.data?.room} fallback={<div>Loading room...</div>}>
@ -276,13 +284,24 @@ export function ChatRoom(props: ChatRoomProps) {
<For each={messages()}>
{(message) => (
<div
class={`message ${message.user.id === props.userId ? 'own-message' : ''}`}
class={`message-wrapper ${message.user.id === props.userId ? 'own-message-wrapper' : 'other-message-wrapper'}`}
>
<div class='message-header'>
<span class='username'>{message.user.username}</span>
<span class='time'>{formatTime(message.createdAt)}</span>
{message.user.id !== props.userId && (
<img
src={getUserAvatarUrl(message.user.id)}
alt={`${message.user.username}'s avatar`}
class='message-avatar'
/>
)}
<div
class={`message ${message.user.id === props.userId ? 'own-message' : 'other-message'}`}
>
<div class='message-header'>
<span class='username'>{message.user.username}</span>
<span class='time'>{formatTime(message.createdAt)}</span>
</div>
<div class='message-content'>{message.content}</div>
</div>
<div class='message-content'>{message.content}</div>
</div>
)}
</For>
@ -295,7 +314,12 @@ export function ChatRoom(props: ChatRoomProps) {
ref={messageInput}
type='text'
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
onInput={(e) => {
const inputValue = e.currentTarget.value;
if (inputValue.length <= MAX_MESSAGE_LENGTH) {
setMessage(inputValue);
}
}}
placeholder='Type a message...'
/>
<button
@ -306,7 +330,18 @@ export function ChatRoom(props: ChatRoomProps) {
😊
</button>
</div>
<button type='submit'>Send</button>
<div class='message-length-counter'>
{message().length}/{MAX_MESSAGE_LENGTH}
</div>
<button
type='submit'
disabled={
message().trim().length === 0 ||
message().length > MAX_MESSAGE_LENGTH
}
>
Send
</button>
{showEmojiPicker() && (
<div ref={emojiPickerContainer} class='emoji-picker-container'>