From f326555c59f74bad3a2cc1d085bdcf43da68af0b Mon Sep 17 00:00:00 2001 From: Lukian Date: Sun, 20 Apr 2025 23:45:33 +0200 Subject: [PATCH] add: added emojis --- back/api/emojis.js | 102 +++++++++++++++++++++ back/api/searchemojis.js | 14 +++ back/libs/mysql.js | 95 +++++++++++++++++++ front/src/components/MessageComponent.tsx | 7 ++ front/src/components/TopBar.tsx | 1 + front/src/main.tsx | 4 + front/src/pages/ChannelPage.tsx | 40 +++++++- front/src/pages/CreateEmoji.tsx | 77 ++++++++++++++++ front/src/pages/EmojisPage.tsx | 107 ++++++++++++++++++++++ front/src/styles/ChannelPage.css | 27 ++++++ front/src/styles/CreateEmoji.css | 15 +++ front/src/styles/EmojisPage.css | 24 +++++ front/src/styles/MessageComponent.css | 5 + front/src/types.tsx | 7 ++ 14 files changed, 523 insertions(+), 2 deletions(-) create mode 100644 back/api/emojis.js create mode 100644 back/api/searchemojis.js create mode 100644 front/src/pages/CreateEmoji.tsx create mode 100644 front/src/pages/EmojisPage.tsx create mode 100644 front/src/styles/CreateEmoji.css create mode 100644 front/src/styles/EmojisPage.css diff --git a/back/api/emojis.js b/back/api/emojis.js new file mode 100644 index 0000000..dafd00b --- /dev/null +++ b/back/api/emojis.js @@ -0,0 +1,102 @@ +const express = require('express'); +const { getConnection, getEmojis, addEmoji, getEmojiByName, deleteEmoji } = require('../libs/mysql'); +const { checkAuth } = require("../libs/middlewares") +const multer = require('multer'); +const fs = require('node:fs'); +const path = require('node:path'); + +const router = express.Router(); + +const upload = multer({ dest: 'data/emojis/' }) +upload.limits = { + fileSize: 1024 * 1024 * 1, + files: 1, +}; + +router.get('/', async (req, res) => { + const connection = await getConnection(); + const emojis = await getEmojis(connection); + connection.end(); + res.send(emojis); +}); + +router.post('/add', upload.single("emoji"), checkAuth, async (req, res) => { + const { name } = req.body; + const file = req.file; + + if (!name || !file) { + return res.status(400).send({ error: 'Invalid name or file' }); + } + + const connection = await getConnection(); + + const emoji = await getEmojiByName(connection, name); + + if (emoji[0]) { + connection.end(); + fs.unlinkSync(`data/emojis/${file.filename}`); + return res.status(400).send({ error: 'Emoji already exists' }); + } + + if (!/^[a-zA-Z0-9-_]+$/.test(name)) { + connection.end(); + fs.unlinkSync(`data/emojis/${file.filename}`); + return res.status(400).send({ error: 'Invalid name' }); + } + + await addEmoji(connection, name, file.filename); + connection.end(); + + req.sockets.emit({ + type: 'new_emoji' + }); + + res.send({ message: "Emoji added" }); +}); + +router.get('/:name', async (req, res) => { + const { name } = req.params; + + const connection = await getConnection(); + const emoji = await getEmojiByName(connection, name); + connection.end(); + + if (!emoji[0]) { + return res.status(404).send({ error: 'Emoji not found' }); + } + + res.sendFile(path.join(__dirname, `../data/emojis/${emoji[0].file}`), { headers: { 'Content-Type': 'image' } }); +}); + +router.post('/:name/delete', checkAuth, async (req, res) => { + const { name } = req.params; + const user = req.user; + + const connection = await getConnection(); + const emoji = await getEmojiByName(connection, name); + + if (!emoji[0]) { + connection.end(); + return res.status(404).send({ error: 'Emoji not found' }); + } + + if (user.admin !== 1) { + connection.end(); + return res.status(401).send({ error: 'Unauthorized' }); + } + + if (fs.existsSync(`data/emojis/${emoji[0].file}`)) { + fs.unlinkSync(`data/emojis/${emoji[0].file}`); + } + + await deleteEmoji(connection, emoji[0].id); + connection.end(); + + req.sockets.emit({ + type: 'delete_emoji', + }); + + res.send({ message: "Emoji deleted" }); +}); + +module.exports = router; \ No newline at end of file diff --git a/back/api/searchemojis.js b/back/api/searchemojis.js new file mode 100644 index 0000000..d9f4e23 --- /dev/null +++ b/back/api/searchemojis.js @@ -0,0 +1,14 @@ +const express = require('express'); +const { getConnection, searchEmojis } = require('../libs/mysql'); + +const router = express.Router(); + +router.get('/', async (req, res) => { + const { search } = req.query; + const connection = await getConnection(); + const emojis = await searchEmojis(connection, search); + connection.end(); + res.send(emojis); +}); + +module.exports = router; \ No newline at end of file diff --git a/back/libs/mysql.js b/back/libs/mysql.js index fadd56f..1fedcdf 100644 --- a/back/libs/mysql.js +++ b/back/libs/mysql.js @@ -477,6 +477,95 @@ function deleteUserMentions(connection, user_id) { }); } +function getEmojis(connection) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT id, name FROM emojis`, + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + +function addEmoji(connection, name, file) { + return new Promise((resolve, reject) => { + connection.query( + `INSERT INTO emojis (name, file) VALUES (?, ?)`, + [name, file], // Use parameterized query + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + +function deleteEmoji(connection, id) { + return new Promise((resolve, reject) => { + connection.query( + `DELETE FROM emojis WHERE id = ?`, + [id], // Use parameterized query + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + +function getEmoji(connection, id) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT * FROM emojis WHERE id = ?`, + [id], // Use parameterized query + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + +function getEmojiByName(connection, name) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT * FROM emojis WHERE name = ?`, + [name], // Use parameterized query + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + +function searchEmojis(connection, search) { + return new Promise((resolve, reject) => { + connection.query( + `SELECT * FROM emojis WHERE name LIKE ? LIMIT 5`, + [`%${search}%`], // Use parameterized query + (error, result) => { + if (error) { + reject(new Error(error)); + } + resolve(result); + } + ); + }); +} + module.exports = { getUsers, getConnection, @@ -508,4 +597,10 @@ module.exports = { deleMentions, deleteUserMentions, deleteChannelMentions, + getEmojis, + addEmoji, + deleteEmoji, + getEmoji, + getEmojiByName, + searchEmojis, }; diff --git a/front/src/components/MessageComponent.tsx b/front/src/components/MessageComponent.tsx index cf9ab4e..f80702a 100644 --- a/front/src/components/MessageComponent.tsx +++ b/front/src/components/MessageComponent.tsx @@ -27,6 +27,10 @@ export default function MessageComponent({ message, user, channel, deleteMessage array.push(string) string = '' array.push(word) + } else if (word.startsWith(":") && word.endsWith(":") && word.length > 2) { + array.push(string) + string = '' + array.push(word) } else { string += word + " " } @@ -47,6 +51,9 @@ export default function MessageComponent({ message, user, channel, deleteMessage return {word} } else if (word.startsWith("https://") || word.startsWith("http://")) { return {word} + } else if (word.startsWith(":") && word.endsWith(":") && word.length > 2) { + const emojiName = word.substring(1, word.length - 1); + return {emojiName} } return {word} ; })} diff --git a/front/src/components/TopBar.tsx b/front/src/components/TopBar.tsx index 4eda32b..56eadfa 100644 --- a/front/src/components/TopBar.tsx +++ b/front/src/components/TopBar.tsx @@ -13,6 +13,7 @@ export default function TopBar({ user }: { user: User | undefined }) { Home Channels Users + Emojis
{user && user.id == 5 ? ( diff --git a/front/src/main.tsx b/front/src/main.tsx index 35fb5bd..3795ea4 100644 --- a/front/src/main.tsx +++ b/front/src/main.tsx @@ -11,6 +11,8 @@ import CreateChannel from './pages/CreateChannel' import ChannelsPage from './pages/ChannelsPage' import UsersPage from './pages/UsersPage' import EditProfile from './pages/EditProfile' +import CreateEmoji from './pages/CreateEmoji' +import EmojisPage from './pages/EmojisPage' import NotFoundPage from './pages/404' const socket = new WebSocket("/api/ws"); @@ -36,6 +38,8 @@ createRoot(document.getElementById('root')!).render( } /> } /> } /> + } /> + } /> } /> diff --git a/front/src/pages/ChannelPage.tsx b/front/src/pages/ChannelPage.tsx index a7b3bf6..33a164e 100644 --- a/front/src/pages/ChannelPage.tsx +++ b/front/src/pages/ChannelPage.tsx @@ -1,6 +1,6 @@ import { useParams, Link, useNavigate } from "react-router-dom"; import React, { useEffect, useState } from "react"; -import { Channel, Messages, User } from "../types"; +import { Channel, Messages, User, Users, Emojis } from "../types"; import TopBar from "../components/TopBar"; import MessageComponent from "../components/MessageComponent"; import axios from "axios"; @@ -16,7 +16,8 @@ export default function ChannelPage({socket}: {socket: WebSocket}) { const [user, setUser] = useState(); const [message, setMessage] = useState(""); const [maxMessageToShown, setMaxMessageToShown] = useState(10); - const [searchedUsers, setSearchedUsers] = useState([]); + const [searchedUsers, setSearchedUsers] = useState([]); + const [searchedEmojis, setSearchedEmojis] = useState([]); const [noChannel, setNoChannel] = useState(false); const ref = React.createRef(); @@ -131,6 +132,20 @@ export default function ChannelPage({socket}: {socket: WebSocket}) { } else { setSearchedUsers([]); } + + if (lastWord && lastWord.startsWith(":")) { + const emojiName = lastWord.slice(1); + axios + .get(`/api/searchemojis?search=${emojiName}`) + .then((res) => { + setSearchedEmojis(res.data); + }) + .catch((err) => { + console.error(err.response.data); + }); + } else { + setSearchedEmojis([]); + } }, [message]); if (noChannel) { @@ -208,6 +223,27 @@ export default function ChannelPage({socket}: {socket: WebSocket}) { ))}
)} + {searchedEmojis.length > 0 && ( +
+ {searchedEmojis.map((emoji) => ( +
+ {emoji.name} + +
+ ))} +
+ )} ) : (
diff --git a/front/src/pages/CreateEmoji.tsx b/front/src/pages/CreateEmoji.tsx new file mode 100644 index 0000000..88c5e63 --- /dev/null +++ b/front/src/pages/CreateEmoji.tsx @@ -0,0 +1,77 @@ +import { useState, useEffect } from "react"; +import { useNavigate } from "react-router-dom"; +import { User } from "../types"; +import axios from "axios"; +import TopBar from "../components/TopBar"; + +import "../styles/CreateEmoji.css"; + +export default function CreateEmoji() { + const navigate = useNavigate(); + const [name, setName] = useState(""); + const [token, setToken] = useState(""); + const [user , setUser] = useState(); + + useEffect(() => { + const localToken = localStorage.getItem("token"); + if (localToken) { + setToken(localToken) + + axios + .post("/api/auth/me", { token: localToken }) + .then((res) => { + setUser(res.data); + }); + } + }, []); + + function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + + const formData = new FormData(); + const fileInput = document.getElementById("emoji") as HTMLInputElement; + if (fileInput.files) { + formData.append("emoji", fileInput.files[0]); + } + formData.append("token", token); + formData.append("name", name); + axios + .post("/api/emojis/add", formData, { + headers: { + "Content-Type": "multipart/form-data", + }, + }) + .then(() => { + navigate("/emojis"); + }) + .catch((err) => { + console.error(err.response.data); + }); + } + + return ( +
+ +
+

Create Emoji

+
+

+ {!/^[a-zA-Z0-9-_]+$/.test(name) && name.length != 0 && ( + Emoji name can only contain letters, numbers, - and _ + )} +

+ setName(e.target.value)} + /> + + +
+
+
+ ); +} \ No newline at end of file diff --git a/front/src/pages/EmojisPage.tsx b/front/src/pages/EmojisPage.tsx new file mode 100644 index 0000000..a5a54e3 --- /dev/null +++ b/front/src/pages/EmojisPage.tsx @@ -0,0 +1,107 @@ +import { useState, useEffect } from "react" +import { Link } from "react-router-dom" +import { Emojis, User } from "../types" +import axios from "axios" +import TopBar from "../components/TopBar" + +import "../styles/EmojisPage.css" + +export default function EmojisPage({socket}: {socket: WebSocket}) { + const [emojis, setEmojis] = useState(); + const [search, setSearch] = useState(""); + const [token, setToken] = useState(""); + const [user, setUser] = useState(); + + function deleteEmoji(name: string) { + if (!window.confirm(`Are you sure you want to delete ${name}?`)) { + return; + } + axios + .post(`/api/emojis/${name}/delete`, { token }) + .then(() => { + window.location.reload(); + }) + .catch((err) => { + console.error(err.response); + }); + } + + useEffect(() => { + const token = localStorage.getItem("token") + + if (token) { + setToken(token) + axios + .post("/api/auth/me", { + token: token + }) + .then((res) => { + setUser(res.data) + }) + } + axios + .get("/api/emojis") + .then((res) => { + setEmojis(res.data) + }) + .catch((err) => { + console.error(err.response) + }) + }, []) + + useEffect(() => { + socket.addEventListener('message', function (event) { + const data = JSON.parse(event.data); + if (data.type === "new_emoji" || data.type === "delete_emoji") { + axios + .get("/api/emojis") + .then((res) => { + setEmojis(res.data) + }) + .catch((err) => { + console.error(err.response) + }) + } + }); + }, []) + + if (!emojis) { + return ( +
+ +
+

Emojis

+

Loading...

+
+
+ ) + } + + return ( +
+ +
+

Channels

+ Create emoji + setSearch(e.target.value)} + /> +
    + {emojis?.sort().filter((emoji) => emoji.name.toLowerCase().includes(search.toLowerCase())).map((emoji) => ( +
  • + {emoji.name} + {user?.admin == 1 && ( + + )} +
  • + ))} +
+
+
+ ) +} \ No newline at end of file diff --git a/front/src/styles/ChannelPage.css b/front/src/styles/ChannelPage.css index 5296825..3dd3fd1 100644 --- a/front/src/styles/ChannelPage.css +++ b/front/src/styles/ChannelPage.css @@ -41,6 +41,33 @@ gap: 5px; } +.emojis { + position: absolute; + top: 100%; + border: 1px solid #270722; + padding: 5px; + display: flex; + flex-direction: column; + justify-content: start; + align-items: left; + gap: 5px; + min-width: 20%; + background-color: white; +} + +.search-emoji { + display: flex; + flex-direction: row; + justify-content: space-between; + align-items: top; + gap: 5px; +} + +.emojis-emoji { + max-width: 1em; + max-height: 1em; +} + .login-prompt { margin: 16px 0; display: flex; diff --git a/front/src/styles/CreateEmoji.css b/front/src/styles/CreateEmoji.css new file mode 100644 index 0000000..6711d69 --- /dev/null +++ b/front/src/styles/CreateEmoji.css @@ -0,0 +1,15 @@ +.create-emoji-page { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + gap: 20px; +} + +.create-emoji { + width: 97%; + border: 1px solid #270722; + padding: 10px; + background-color: #fff6fd; +} \ No newline at end of file diff --git a/front/src/styles/EmojisPage.css b/front/src/styles/EmojisPage.css new file mode 100644 index 0000000..55e211e --- /dev/null +++ b/front/src/styles/EmojisPage.css @@ -0,0 +1,24 @@ +.emojis-page { + display: flex; + flex-direction: column; + justify-content: start; + align-items: center; + width: 100%; + gap: 20px; +} + +.emojis-page-emojis { + width: 97%; + border: 1px solid #270722; + padding: 10px; + background-color: #fff6fd; + display: flex; + flex-direction: column; + align-items: start; + gap: 10px; +} + +.emoji { + max-width: 50px; + max-height: 50px; +} \ No newline at end of file diff --git a/front/src/styles/MessageComponent.css b/front/src/styles/MessageComponent.css index 91624bb..a22fe87 100644 --- a/front/src/styles/MessageComponent.css +++ b/front/src/styles/MessageComponent.css @@ -31,3 +31,8 @@ overflow: hidden; text-overflow: ellipsis; } + +.message-emoji { + max-width: 1em; + max-height: 1em; +} diff --git a/front/src/types.tsx b/front/src/types.tsx index 4d2504d..75641ef 100644 --- a/front/src/types.tsx +++ b/front/src/types.tsx @@ -46,3 +46,10 @@ export type User = { } export type Users = User[] + +export type Emoji = { + id: number + name: string +} + +export type Emojis = Emoji[]