feat: add emoji picker to chat room message input

- Integrated emoji-picker-element library for emoji selection
- Added emoji toggle button in message input container
- Implemented emoji picker show/hide functionality
- Created CSS styles for emoji picker positioning
- Added event handling for emoji selection and outside click
- Updated package.json to include emoji-picker-element dependency
- Modified Prisma schema to use TEXT type for message content
- Updated Prisma migration scripts to use dotenv for environment configuration
This commit is contained in:
Juan Sebastián Montoya 2025-03-07 00:41:25 -05:00
parent 1e3b188d90
commit 3d41e2cc42
7 changed files with 111 additions and 11 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": "prisma generate",
"prisma:migrate": "prisma migrate dev",
"prisma:init": "prisma migrate dev --name init",
"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",
"build": "tsc"
},
"keywords": [],

View file

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `Message` MODIFY `content` TEXT NOT NULL;

View file

@ -40,7 +40,7 @@ model Room {
model Message {
id String @id @default(uuid())
content String
content String @db.Text
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
userId String

View file

@ -11,6 +11,7 @@
"dependencies": {
"@urql/core": "^5.1.1",
"@urql/solid": "^0.1.2",
"emoji-picker-element": "^1.26.1",
"graphql-ws": "^6.0.4",
"solid-js": "^1.9.5",
"zod": "^3.24.2"

View file

@ -449,3 +449,32 @@ button:disabled {
height: 50vh;
}
}
/* Emoji Picker Styles */
.message-input-container {
display: flex;
align-items: center;
flex: 1;
}
.emoji-toggle-button {
background: none;
border: none;
font-size: 1.5rem;
cursor: pointer;
margin-left: 0.5rem;
padding: 0.25rem;
line-height: 1;
}
.emoji-picker-container {
position: absolute;
bottom: 100%;
right: 0;
z-index: 10;
margin-bottom: 0.5rem;
}
.message-form {
position: relative;
}

View file

@ -1,7 +1,10 @@
import { createSignal, createEffect, For, Show } from 'solid-js';
import { createSignal, createEffect, For, Show, onMount } from 'solid-js';
import { gql } from '@urql/core';
import { createQuery, createMutation, createSubscription } from '@urql/solid';
import { Message } from '../types';
import 'emoji-picker-element';
import { EmojiClickEvent } from 'emoji-picker-element/shared';
import { Picker } from 'emoji-picker-element';
const ROOM_QUERY = gql`
query GetRoom($id: ID!) {
@ -80,7 +83,13 @@ export function ChatRoom(props: ChatRoomProps) {
const [messages, setMessages] = createSignal<Message[]>([]);
const [confirmLeave, setConfirmLeave] = createSignal(false);
const [leaveError, setLeaveError] = createSignal<string | null>(null);
const [showEmojiPicker, setShowEmojiPicker] = createSignal(false);
const [emojiPickerRef, setPickerRef] = createSignal<Picker | undefined>(
undefined
);
let messagesContainer: HTMLDivElement | undefined;
let messageInput: HTMLInputElement | undefined;
let emojiPickerContainer: HTMLDivElement | undefined;
const scrollToBottom = () => {
if (messagesContainer) {
@ -198,6 +207,41 @@ export function ChatRoom(props: ChatRoomProps) {
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
};
const toggleEmojiPicker = () => {
setShowEmojiPicker(!showEmojiPicker());
};
createEffect(() => {
const handleEmojiSelect = (event: EmojiClickEvent) => {
setMessage((prev) => prev + event.detail.unicode);
messageInput?.focus();
};
const ref = emojiPickerRef();
if (ref) {
ref.addEventListener('emoji-click', handleEmojiSelect);
return () => {
ref.removeEventListener('emoji-click', handleEmojiSelect);
};
}
});
// Close emoji picker when clicking outside
onMount(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
emojiPickerContainer &&
!emojiPickerContainer.contains(event.target as Node)
) {
setShowEmojiPicker(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
});
return (
<div class='chat-room'>
<Show when={roomQuery.data?.room} fallback={<div>Loading room...</div>}>
@ -246,13 +290,30 @@ export function ChatRoom(props: ChatRoomProps) {
</div>
<form class='message-form' onSubmit={handleSendMessage}>
<input
type='text'
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder='Type a message...'
/>
<div class='message-input-container'>
<input
ref={messageInput}
type='text'
value={message()}
onInput={(e) => setMessage(e.currentTarget.value)}
placeholder='Type a message...'
/>
<button
type='button'
class='emoji-toggle-button'
onClick={toggleEmojiPicker}
>
😊
</button>
</div>
<button type='submit'>Send</button>
{showEmojiPicker() && (
<div ref={emojiPickerContainer} class='emoji-picker-container'>
{/* @ts-ignore */}
<emoji-picker ref={(el: Picker) => setPickerRef(el)}></emoji-picker>
</div>
)}
</form>
</div>
);

7
package-lock.json generated
View file

@ -48,6 +48,7 @@
"dependencies": {
"@urql/core": "^5.1.1",
"@urql/solid": "^0.1.2",
"emoji-picker-element": "^1.26.1",
"graphql-ws": "^6.0.4",
"solid-js": "^1.9.5",
"zod": "^3.24.2"
@ -4381,6 +4382,12 @@
"integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==",
"license": "ISC"
},
"node_modules/emoji-picker-element": {
"version": "1.26.1",
"resolved": "https://registry.npmjs.org/emoji-picker-element/-/emoji-picker-element-1.26.1.tgz",
"integrity": "sha512-XgQ9s2JdmworiqLfJC7eGbzQHGv8yb8U9XofjeRAnOMYaeLh0MfwVAz9oG1YE2U2WnzU0Pys1axMjYtPKJ7YSg==",
"license": "Apache-2.0"
},
"node_modules/emoji-regex": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",