add: improved frontend and added descriptions to users

This commit is contained in:
Lukian 2025-05-17 00:22:31 +02:00
parent 6342377aa0
commit eca9efc170
14 changed files with 266 additions and 29 deletions

View file

@ -1,7 +1,7 @@
const express = require('express'); const express = require('express');
const sha256 = require("sha256"); const sha256 = require("sha256");
const jwt = require('jsonwebtoken'); const jwt = require('jsonwebtoken');
const { getConnection, getUserByUsername, addUser, setUserPfp, setUserUsername, setUserPassword } = require('../libs/mysql'); const { getConnection, getUserByUsername, addUser, setUserPfp, setUserUsername, setUserPassword, setUserDescription } = require('../libs/mysql');
const { checkAuth } = require('../libs/middlewares'); const { checkAuth } = require('../libs/middlewares');
const multer = require('multer') const multer = require('multer')
const rateLimit = require("express-rate-limit"); const rateLimit = require("express-rate-limit");
@ -82,7 +82,7 @@ router.post('/register', speedLimiter, limiter, async (req, res) => {
router.post('/me', checkAuth, async (req, res) => { router.post('/me', checkAuth, async (req, res) => {
const user = req.user; const user = req.user;
res.send({ id: user.id, username: user.username, admin: user.admin }); res.send({ id: user.id, username: user.username, admin: user.admin , description: user.description });
}); });
router.post('/me/uploadpfp', upload.single('pfp'), checkAuth, async (req, res) => { router.post('/me/uploadpfp', upload.single('pfp'), checkAuth, async (req, res) => {
@ -151,4 +151,18 @@ router.post('/me/setpassword', checkAuth, async (req, res) => {
res.send({ message: 'Password changed.' }); res.send({ message: 'Password changed.' });
}); });
router.post('/me/setdescription', checkAuth, async (req, res) => {
const { description } = req.body;
const user = req.user;
if (!description) {
return res.status(400).send({ error: 'Invalid description' });
}
const connection = await getConnection();
await setUserDescription(connection, user.id, description);
connection.end();
res.send({ message: 'Description changed.' });
});
module.exports = router; module.exports = router;

View file

@ -37,7 +37,7 @@ router.get('/:username', async (req, res) => {
const user = await getUserByUsername(connection, username); const user = await getUserByUsername(connection, username);
connection.end(); connection.end();
if (user[0]) { if (user[0]) {
res.send({id: user[0].id, username: user[0].username, admin: user[0].admin}); res.send({id: user[0].id, username: user[0].username, admin: user[0].admin, description: user[0].description});
} else { } else {
return res.status(400).send({ error: 'No user found' }); return res.status(400).send({ error: 'No user found' });
} }

View file

@ -47,7 +47,7 @@ function deleteUser(connection, id) {
function getUsers(connection) { function getUsers(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT id, username, admin FROM users`, `SELECT id, username, admin, description FROM users`,
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -148,6 +148,21 @@ function setUserPassword(connection, id, password) {
}); });
} }
function setUserDescription(connection, id, description) {
return new Promise((resolve, reject) => {
connection.query(
`UPDATE users SET description = ? WHERE id = ?`,
[description, id],
(error, result) => {
if (error) {
reject(new Error(error));
}
resolve(result);
}
);
});
}
// +---------------------------+ // +---------------------------+
// | Channels | // | Channels |
// +---------------------------+ // +---------------------------+
@ -185,7 +200,9 @@ function deleteChannel(connection, channel_id) {
function getChannels(connection) { function getChannels(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT * FROM channels`, `SELECT channels.id, name, channels.description, owner_id, username AS owner_username
FROM channels
JOIN users ON channels.owner_id = users.id`,
(error, result) => { (error, result) => {
if (error) { if (error) {
reject(new Error(error)); reject(new Error(error));
@ -199,7 +216,7 @@ function getChannels(connection) {
function getChannel(connection, name) { function getChannel(connection, name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT channels.id, name, description, owner_id, username AS owner_username `SELECT channels.id, name, channels.description, owner_id, username AS owner_username
FROM channels FROM channels
JOIN users ON channels.owner_id = users.id JOIN users ON channels.owner_id = users.id
WHERE name = ?`, WHERE name = ?`,
@ -217,7 +234,7 @@ function getChannel(connection, name) {
function getActiveChannels(connection) { function getActiveChannels(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT channels.id, name, description, owner_id, username AS owner_username, count(*) AS message_count `SELECT channels.id, name, channels.description, owner_id, username AS owner_username, count(*) AS message_count
FROM messages FROM messages
JOIN channels ON messages.channel_id = channels.id JOIN channels ON messages.channel_id = channels.id
JOIN users ON messages.user_id = users.id JOIN users ON messages.user_id = users.id
@ -238,7 +255,7 @@ function getActiveChannels(connection) {
function getNewChannels(connection) { function getNewChannels(connection) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT channels.id, name, description, owner_id, username AS owner_username `SELECT channels.id, name, channels.description, owner_id, username AS owner_username
FROM channels FROM channels
JOIN users ON channels.owner_id = users.id JOIN users ON channels.owner_id = users.id
ORDER BY channels.id DESC LIMIT 5`, ORDER BY channels.id DESC LIMIT 5`,
@ -255,7 +272,7 @@ function getNewChannels(connection) {
function searchChannels(connection, search) { function searchChannels(connection, search) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
connection.query( connection.query(
`SELECT channels.id, name, description, owner_id, username AS owner_username `SELECT channels.id, name, channels.description, owner_id, username AS owner_username
FROM channels FROM channels
JOIN users ON channels.owner_id = users.id JOIN users ON channels.owner_id = users.id
WHERE name LIKE ? WHERE name LIKE ?
@ -673,6 +690,7 @@ module.exports = {
setUserPfp, setUserPfp,
setUserUsername, setUserUsername,
setUserPassword, setUserPassword,
setUserDescription,
addChannel, addChannel,
deleteChannel, deleteChannel,

View file

@ -75,10 +75,11 @@ label {
.form-horizontal { .form-horizontal {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: start;
align-items: center; align-items: center;
gap: 5px; gap: 5px;
position: relative; position: relative;
flex-wrap: wrap;
} }
.form-vertical { .form-vertical {
@ -147,7 +148,7 @@ label {
.forum-button { .forum-button {
border: 1px solid #a678af; border: 1px solid #a678af;
background-color: #847996; background-color: #7d5a81;
color: black; color: black;
} }

View file

@ -4,6 +4,8 @@ import { Channels, User } from "../types"
import axios from "axios" import axios from "axios"
import TopBar from "../components/TopBar" import TopBar from "../components/TopBar"
import "../styles/ChannelsPage.css"
export default function ChannelsPage({socket}: {socket: WebSocket}) { export default function ChannelsPage({socket}: {socket: WebSocket}) {
const [channels, setChannels] = useState<Channels>(); const [channels, setChannels] = useState<Channels>();
const [search, setSearch] = useState<string>(""); const [search, setSearch] = useState<string>("");
@ -85,18 +87,22 @@ export default function ChannelsPage({socket}: {socket: WebSocket}) {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<ul> <div className="forum-channels">
{channels?.sort().filter((channel) => channel.name.toLowerCase().includes(search.toLowerCase())).map((channel) => ( {channels?.sort().filter((channel) => channel.name.toLowerCase().includes(search.toLowerCase())).map((channel) => (
<li key={channel.id}> <div key={channel.id} className="forum-channel">
<Link to={`/c/${channel.name}`}>{channel.name}</Link> <div className="forum-channel-left">
<h3><Link to={`/c/${channel.name}`}>{channel.name}</Link></h3>
<p>{channel.description}</p>
<p>Owner : <Link to={`/u/${channel.owner_username}`}>{channel.owner_username}</Link></p>
</div>
{user?.admin == 1 && ( {user?.admin == 1 && (
<button onClick={() => deleteChannel(channel.name)} className="forum-button"> <button onClick={() => deleteChannel(channel.name)} className="forum-button">
Delete Delete
</button> </button>
)} )}
</li> </div>
))} ))}
</ul> </div>
</div> </div>
</div> </div>
) )

View file

@ -7,6 +7,7 @@ import TopBar from "../components/TopBar"
export default function EditProfile() { export default function EditProfile() {
const [token, setToken] = useState<string>(""); const [token, setToken] = useState<string>("");
const [user, setUser] = useState<User>(); const [user, setUser] = useState<User>();
const [description, setDescription] = useState<string>("");
useEffect(() => { useEffect(() => {
const localToken = localStorage.getItem("token"); const localToken = localStorage.getItem("token");
@ -88,6 +89,18 @@ export default function EditProfile() {
}); });
} }
function editDescription(e: React.FormEvent) {
e.preventDefault();
axios
.post("/api/auth/me/setdescription", { token: token, description: description })
.then(() => {
window.location.reload();
})
.catch((err) => {
console.error(err.response.data);
});
}
if (!user) { if (!user) {
return ( return (
<div className="forum-page"> <div className="forum-page">
@ -121,6 +134,20 @@ export default function EditProfile() {
<button onClick={editUsername} className="forum-button">Save</button> <button onClick={editUsername} className="forum-button">Save</button>
</form> </form>
</div> </div>
<div className="forum-section">
<h2>Edit description</h2>
<form className="form-horizontal">
<textarea
name="description"
id="description"
placeholder={user?.description}
className="forum-input"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<button onClick={editDescription} className="forum-button">Save</button>
</form>
</div>
<div className="forum-section"> <div className="forum-section">
<h2>Edit Password</h2> <h2>Edit Password</h2>
<form className="form-horizontal"> <form className="form-horizontal">

View file

@ -90,9 +90,9 @@ export default function EmojisPage({socket}: {socket: WebSocket}) {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<ul> <div className="forum-emojis">
{emojis?.sort().filter((emoji) => emoji.name.toLowerCase().includes(search.toLowerCase())).map((emoji) => ( {emojis?.sort().filter((emoji) => emoji.name.toLowerCase().includes(search.toLowerCase())).map((emoji) => (
<li key={emoji.id}> <div key={emoji.id} className="forum-emoji">
<img src={`/api/emojis/${emoji.name}`} alt={emoji.name} className="emoji-fat" /> <img src={`/api/emojis/${emoji.name}`} alt={emoji.name} className="emoji-fat" />
<span>:{emoji.name}:</span> <span>:{emoji.name}:</span>
{user?.admin == 1 && ( {user?.admin == 1 && (
@ -100,9 +100,9 @@ export default function EmojisPage({socket}: {socket: WebSocket}) {
Delete Delete
</button> </button>
)} )}
</li> </div>
))} ))}
</ul> </div>
</div> </div>
</div> </div>
) )

View file

@ -127,6 +127,9 @@ export default function UserPage({socket}: {socket: WebSocket}) {
<img src={`/api/users/${pageUser.username}/pfp`} alt="pfp" className="user-page-pfp" /> <img src={`/api/users/${pageUser.username}/pfp`} alt="pfp" className="user-page-pfp" />
<h2>{pageUser.username}</h2> <h2>{pageUser.username}</h2>
</div> </div>
<p>
{pageUser.description ? pageUser.description : "No description"}
</p>
{pageUser.admin ? <p>Admin</p> : <p>User</p>} {pageUser.admin ? <p>Admin</p> : <p>User</p>}
{pageUser.id === user?.id && ( {pageUser.id === user?.id && (
<Link to="/edit-profile">Edit profile</Link> <Link to="/edit-profile">Edit profile</Link>

View file

@ -4,6 +4,8 @@ import { User, Users } from "../types"
import axios from "axios" import axios from "axios"
import TopBar from "../components/TopBar" import TopBar from "../components/TopBar"
import "../styles/UsersPage.css"
export default function UsersPage({socket}: {socket: WebSocket}) { export default function UsersPage({socket}: {socket: WebSocket}) {
const [users, setUsers] = useState<Users>(); const [users, setUsers] = useState<Users>();
const [search, setSearch] = useState<string>(""); const [search, setSearch] = useState<string>("");
@ -84,18 +86,31 @@ export default function UsersPage({socket}: {socket: WebSocket}) {
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<ul> <div className="forum-users">
{users?.sort().filter((channel) => channel.username.toLowerCase().includes(search.toLowerCase())).map((user) => ( {users?.sort().filter((channel) => channel.username.toLowerCase().includes(search.toLowerCase())).map((user) => (
<li key={user.id}> <div key={user.id} className="forum-user">
<Link to={`/u/${user.username}`}>{user.username}</Link> <div className="forum-user-left">
<img src={`/api/users/${user.username}/pfp`} alt="pfp" className="forum-user-pfp" />
<div className="forum-user-info">
<div className="forum-user-title">
<h3><Link to={`/u/${user.username}`}>{user.username}</Link></h3>
{user.admin == 1 && <span className="forum-user-admin">Admin</span>}
</div>
{user.description ? (
<p className="forum-user-description">{user.description}</p>
) : (
<p className="forum-user-description">No description</p>
)}
</div>
</div>
{thisUser?.admin == 1 && ( {thisUser?.admin == 1 && (
<button onClick={() => deleteUser(user.username)} className="forum-button"> <button onClick={() => deleteUser(user.username)} className="forum-button">
Delete Delete
</button> </button>
)} )}
</li> </div>
))} ))}
</ul> </div>
</div> </div>
</div> </div>
) )

View file

@ -0,0 +1,50 @@
.forum-channels {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.forum-channel {
width: 97%;
border: 1px solid #270722;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
gap: 5px;
padding: 10px;
}
.forum-channel-left {
display: flex;
flex-direction: column;
align-items: start;
padding: 5px;
gap: 5px;
}
.forum-channel-title {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: 15px;
}
.forum-channel-left h3 {
margin: 0;
}
.forum-channel-left p {
margin: 0;
}
@media (prefers-color-scheme: dark) {
.forum-channel {
border: 1px solid #a678af;
}
}

View file

@ -1,4 +1,31 @@
.forum-emojis {
width: 100%;
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: start;
align-items: start;
gap: 10px;
}
.forum-emoji {
width: 150px;
border: 1px solid #270722;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 5px;
padding: 10px;
}
.emoji-fat { .emoji-fat {
max-width: 2em; max-width: 100px;
max-height: 2em; max-height: 100px;
} }
@media (prefers-color-scheme: dark) {
.forum-emoji {
border: 1px solid #a678af;
}
}

View file

@ -63,10 +63,12 @@
} }
.messages { .messages {
order: 2;
width: 97%; width: 97%;
} }
.channels { .channels {
order: 1;
width: 97%; width: 97%;
} }
} }

View file

@ -0,0 +1,74 @@
.forum-users {
width: 100%;
display: flex;
flex-direction: column;
justify-content: space-between;
align-items: center;
gap: 10px;
}
.forum-user {
width: 97%;
border: 1px solid #270722;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: start;
gap: 5px;
padding: 10px;
}
.forum-user-left {
display: flex;
flex-direction: row;
align-items: start;
padding: 5px;
gap: 15px;
}
.forum-user-pfp {
width: 40px;
height: 40px;
border-radius: 50%;
}
.forum-user-info {
display: flex;
flex-direction: column;
justify-content: start;
align-items: start;
gap: 5px;
}
.forum-user-title {
display: flex;
flex-direction: row;
justify-content: start;
align-items: center;
gap: 15px;
}
.forum-user-admin {
background-color: #ebb8e6;
padding: 2px 5px;
}
.forum-user-title h3 {
margin: 0;
}
.forum-user-left p {
margin: 0;
}
@media (prefers-color-scheme: dark) {
.forum-user {
border: 1px solid #a678af;
}
.forum-user-admin {
background-color: #a678af;
padding: 2px 5px;
}
}

View file

@ -54,7 +54,7 @@ export type User = {
id: number id: number
username: string username: string
admin: number admin: number
pfp: string description: string
} }
export type Users = User[] export type Users = User[]