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:
parent
3d41e2cc42
commit
d4d99fb5e7
5 changed files with 144 additions and 24 deletions
|
@ -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": [],
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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'>
|
||||
|
|
Loading…
Add table
Reference in a new issue