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 sha256 = require("sha256");
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 multer = require('multer')
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) => {
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) => {
@ -151,4 +151,18 @@ router.post('/me/setpassword', checkAuth, async (req, res) => {
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;

View file

@ -37,7 +37,7 @@ router.get('/:username', async (req, res) => {
const user = await getUserByUsername(connection, username);
connection.end();
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 {
return res.status(400).send({ error: 'No user found' });
}

View file

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

View file

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

View file

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

View file

@ -7,6 +7,7 @@ import TopBar from "../components/TopBar"
export default function EditProfile() {
const [token, setToken] = useState<string>("");
const [user, setUser] = useState<User>();
const [description, setDescription] = useState<string>("");
useEffect(() => {
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) {
return (
<div className="forum-page">
@ -121,6 +134,20 @@ export default function EditProfile() {
<button onClick={editUsername} className="forum-button">Save</button>
</form>
</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">
<h2>Edit Password</h2>
<form className="form-horizontal">

View file

@ -90,9 +90,9 @@ export default function EmojisPage({socket}: {socket: WebSocket}) {
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
<div className="forum-emojis">
{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" />
<span>:{emoji.name}:</span>
{user?.admin == 1 && (
@ -100,9 +100,9 @@ export default function EmojisPage({socket}: {socket: WebSocket}) {
Delete
</button>
)}
</li>
</div>
))}
</ul>
</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" />
<h2>{pageUser.username}</h2>
</div>
<p>
{pageUser.description ? pageUser.description : "No description"}
</p>
{pageUser.admin ? <p>Admin</p> : <p>User</p>}
{pageUser.id === user?.id && (
<Link to="/edit-profile">Edit profile</Link>

View file

@ -4,6 +4,8 @@ import { User, Users } from "../types"
import axios from "axios"
import TopBar from "../components/TopBar"
import "../styles/UsersPage.css"
export default function UsersPage({socket}: {socket: WebSocket}) {
const [users, setUsers] = useState<Users>();
const [search, setSearch] = useState<string>("");
@ -84,18 +86,31 @@ export default function UsersPage({socket}: {socket: WebSocket}) {
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<ul>
<div className="forum-users">
{users?.sort().filter((channel) => channel.username.toLowerCase().includes(search.toLowerCase())).map((user) => (
<li key={user.id}>
<Link to={`/u/${user.username}`}>{user.username}</Link>
<div key={user.id} className="forum-user">
<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 && (
<button onClick={() => deleteUser(user.username)} className="forum-button">
Delete
</button>
)}
</li>
</div>
))}
</ul>
</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 @@
.emoji-fat {
max-width: 2em;
max-height: 2em;
.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 {
max-width: 100px;
max-height: 100px;
}
@media (prefers-color-scheme: dark) {
.forum-emoji {
border: 1px solid #a678af;
}
}

View file

@ -63,10 +63,12 @@
}
.messages {
order: 2;
width: 97%;
}
.channels {
order: 1;
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
username: string
admin: number
pfp: string
description: string
}
export type Users = User[]