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:
parent
1e3b188d90
commit
3d41e2cc42
7 changed files with 111 additions and 11 deletions
|
@ -10,9 +10,9 @@
|
||||||
"test": "ts-node --test test/**/*.test.ts",
|
"test": "ts-node --test test/**/*.test.ts",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"dev": "nodemon --delay 2000ms src/index.ts",
|
"dev": "nodemon --delay 2000ms src/index.ts",
|
||||||
"prisma:generate": "prisma generate",
|
"prisma:generate": "dotenv -e ../../.env -- prisma generate",
|
||||||
"prisma:migrate": "prisma migrate dev",
|
"prisma:migrate": "dotenv -e ../../.env -- prisma migrate dev",
|
||||||
"prisma:init": "prisma migrate dev --name init",
|
"prisma:init": "dotenv -e ../../.env -- prisma migrate dev --name init",
|
||||||
"build": "tsc"
|
"build": "tsc"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE `Message` MODIFY `content` TEXT NOT NULL;
|
|
@ -40,7 +40,7 @@ model Room {
|
||||||
|
|
||||||
model Message {
|
model Message {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
content String
|
content String @db.Text
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
userId String
|
userId String
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urql/core": "^5.1.1",
|
"@urql/core": "^5.1.1",
|
||||||
"@urql/solid": "^0.1.2",
|
"@urql/solid": "^0.1.2",
|
||||||
|
"emoji-picker-element": "^1.26.1",
|
||||||
"graphql-ws": "^6.0.4",
|
"graphql-ws": "^6.0.4",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
|
|
|
@ -449,3 +449,32 @@ button:disabled {
|
||||||
height: 50vh;
|
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;
|
||||||
|
}
|
||||||
|
|
|
@ -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 { gql } from '@urql/core';
|
||||||
import { createQuery, createMutation, createSubscription } from '@urql/solid';
|
import { createQuery, createMutation, createSubscription } from '@urql/solid';
|
||||||
import { Message } from '../types';
|
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`
|
const ROOM_QUERY = gql`
|
||||||
query GetRoom($id: ID!) {
|
query GetRoom($id: ID!) {
|
||||||
|
@ -80,7 +83,13 @@ export function ChatRoom(props: ChatRoomProps) {
|
||||||
const [messages, setMessages] = createSignal<Message[]>([]);
|
const [messages, setMessages] = createSignal<Message[]>([]);
|
||||||
const [confirmLeave, setConfirmLeave] = createSignal(false);
|
const [confirmLeave, setConfirmLeave] = createSignal(false);
|
||||||
const [leaveError, setLeaveError] = createSignal<string | null>(null);
|
const [leaveError, setLeaveError] = createSignal<string | null>(null);
|
||||||
|
const [showEmojiPicker, setShowEmojiPicker] = createSignal(false);
|
||||||
|
const [emojiPickerRef, setPickerRef] = createSignal<Picker | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
let messagesContainer: HTMLDivElement | undefined;
|
let messagesContainer: HTMLDivElement | undefined;
|
||||||
|
let messageInput: HTMLInputElement | undefined;
|
||||||
|
let emojiPickerContainer: HTMLDivElement | undefined;
|
||||||
|
|
||||||
const scrollToBottom = () => {
|
const scrollToBottom = () => {
|
||||||
if (messagesContainer) {
|
if (messagesContainer) {
|
||||||
|
@ -198,6 +207,41 @@ export function ChatRoom(props: ChatRoomProps) {
|
||||||
return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
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 (
|
return (
|
||||||
<div class='chat-room'>
|
<div class='chat-room'>
|
||||||
<Show when={roomQuery.data?.room} fallback={<div>Loading room...</div>}>
|
<Show when={roomQuery.data?.room} fallback={<div>Loading room...</div>}>
|
||||||
|
@ -246,13 +290,30 @@ export function ChatRoom(props: ChatRoomProps) {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form class='message-form' onSubmit={handleSendMessage}>
|
<form class='message-form' onSubmit={handleSendMessage}>
|
||||||
<input
|
<div class='message-input-container'>
|
||||||
type='text'
|
<input
|
||||||
value={message()}
|
ref={messageInput}
|
||||||
onInput={(e) => setMessage(e.currentTarget.value)}
|
type='text'
|
||||||
placeholder='Type a message...'
|
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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
7
package-lock.json
generated
7
package-lock.json
generated
|
@ -48,6 +48,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@urql/core": "^5.1.1",
|
"@urql/core": "^5.1.1",
|
||||||
"@urql/solid": "^0.1.2",
|
"@urql/solid": "^0.1.2",
|
||||||
|
"emoji-picker-element": "^1.26.1",
|
||||||
"graphql-ws": "^6.0.4",
|
"graphql-ws": "^6.0.4",
|
||||||
"solid-js": "^1.9.5",
|
"solid-js": "^1.9.5",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2"
|
||||||
|
@ -4381,6 +4382,12 @@
|
||||||
"integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==",
|
"integrity": "sha512-oen93kVyqSb3l+ziUgzIOlWt/oOuy4zRmpwestMn4rhFWAoFJeFuCVte9F2fASjeZZo7l/Cif9TiyrdW4CwEMA==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/emoji-regex": {
|
||||||
"version": "8.0.0",
|
"version": "8.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||||
|
|
Loading…
Add table
Reference in a new issue