Implemented a delete button for mods and fixed an issue in the backend

This commit is contained in:
Gu://em_ 2025-05-17 02:27:56 +02:00
parent 60e557826e
commit 5af16b593c
7 changed files with 240 additions and 45 deletions

View file

@ -14,6 +14,6 @@
"auth": {
"JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY",
"tokenExpiry": "1h"
"tokenExpiry": "30d"
}
}

View file

@ -53,7 +53,7 @@ async function getModByName(req, res) {
async function deleteMod(req, res) {
try {
// Authorize
authorizeModModification(req);
await authorizeModModification(req);
// Query
const name = req.params.name
const query_result = await mod_service.deleteMod(name);

View file

@ -1,6 +1,8 @@
// Preact
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode'
// Functions
import { createMod } from '../services/mods';
@ -18,13 +20,113 @@ import styles from '../styles/content_creation.module.css'
import Button from '../components/Buttons/button';
function ModCreationPage() {
//TODO add missing fields
const user = "guill" //TODO
const mod_infos = {
name: ""
const null_fields = {
name: null,
display_name: null,
description: null,
mod_infos: {
full_description: null
}
}
//TODO use a service
let user;
const token = Cookies.get('authToken');
if (token) {
const decoded_token = jwtDecode(token);
if (decoded_token) {
user = decoded_token.username;
} else {
location.replace('/login')
}
} else {
location.replace('/login')
}
const [name, setName] = useState('');
const [display_name, setDisplayName] = useState('');
const [description, setDescription] = useState('');
const [full_description, setFullDescription] = useState('');
const [publish_status, setPublishStatus] = useState(null);
const [field_errors, setFieldErrors] = useState(null_fields);
// Handle fields changes
const handleNameChange = (event) => {
setName(event.target.value);
// Reset error
setFieldErrors(prevErrors => ({ ...prevErrors, name: null }));
};
const handleDisplayNameChange = (event) => {
setDisplayName(event.target.value);
setFieldErrors(prevErrors => ({ ...prevErrors, display_name: null }));
};
const handleDescriptionChange = (event) => {
setDescription(event.target.value);
setFieldErrors(prevErrors => ({ ...prevErrors, description: null }));
};
const handleFullDescriptionChange = (event) => {
setFullDescription(event.target.value);
console.debug(event.target.value);
setFieldErrors(prevErrors => ({ ...prevErrors, full_description: null }));
};
// Submission
const handleSubmit = async (event) => {
event.preventDefault();
setFieldErrors(null_fields);
setPublishStatus('publishing');
try {
// Gather data
const mod_data = {
name: name,
display_name: display_name,
description: description,
mod_infos: {
full_description: full_description,
//TODO handle all possible fields
"license": {
"type": "none"
},
"links": []
}
}
// Query
const response = await createMod(mod_data);
// On success
console.debug('Published successfully:', response);
setPublishStatus('success');
window.location.replace("/mods/" + name); //TODO no page reloading
} catch (error) {
console.error('Creation failed:', error);
setPublishStatus('error');
//TODO handle different codes differently
setFieldErrors({
name: ' ',
display_name: ' ',
description: ' ',
mod_infos: {
full_description: ' '
}
});
}
finally {
setPublishStatus(null);
}
};
return (
<>
<a href="/">
@ -33,7 +135,7 @@ function ModCreationPage() {
</a>
<div className={styles.container}>
<div className={styles.form}>
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.topFields}>
<div>
@ -41,9 +143,9 @@ function ModCreationPage() {
<InputField
id="name"
name="name"
value={mod_infos.name}
// onChange={handleNameChange}
// error={nameError}
value={name}
onChange={handleNameChange}
error={field_errors.name}
placeholder="name"
required
className={styles.smallField}
@ -54,9 +156,9 @@ function ModCreationPage() {
<InputField
id="display_name"
name="display_name"
value={mod_infos.display_name}
// onChange={handleNameChange}
// error={nameError}
value={display_name}
onChange={handleDisplayNameChange}
error={field_errors.display_name}
placeholder="display_name"
className={styles.smallField}
>
@ -67,9 +169,9 @@ function ModCreationPage() {
<TextArea
id="description"
name="description"
value={mod_infos.display_name}
// onChange={handleNameChange}
// error={nameError}
value={description}
onChange={handleDescriptionChange}
error={field_errors.description}
placeholder="description"
containerClass={styles.descriptionField}
>
@ -81,9 +183,9 @@ function ModCreationPage() {
<TextArea
id="full_description"
name="full_description"
value={mod_infos.display_name}
// onChange={handleNameChange}
// error={nameError}
value={full_description}
onChange={handleFullDescriptionChange}
error={field_errors.mod_infos.full_description}
placeholder="full_description"
containerClass={styles.fullDescriptionField}
>
@ -92,17 +194,15 @@ function ModCreationPage() {
</div>
<Button className={styles.createButton}>
Create
Publish
</Button>
</div>
</form>
<div className={styles.infosPanel}>
<a href={"/users/" + user}>
<img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{user}</p>
</a>
<img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{user}</p>
</div>
</div>

View file

@ -1,11 +1,14 @@
// Preact
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode'
// Functions
import { getMod } from '../services/mods';
import { getMod, deleteMod } from '../services/mods';
// Components
import Button from '../components/Buttons/button';
// Images
import logo from '../assets/logo.png'
@ -22,23 +25,47 @@ function ModPage({name}) {
const [mod, setMod] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [owner, setOwner] = useState(false);
// UseEffect
useEffect(() => {
async function loadItems() {
setLoading(true);
setError(false);
try {
const fetched_mod = await getMod(name);
setMod(fetched_mod);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
useEffect(() => {
// Load mod informations
async function loadItems() {
setLoading(true);
setError(false);
try {
const fetched_mod = await getMod(name);
setMod(fetched_mod);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadItems();
}, []); // <-- Tells useEffect to run once after render
// Handles
const handleDeleteMod = async () => {
await deleteMod(mod.name);
location.replace('/dashboard');
};
// Load user informations
//TODO use a service
//? for some reason, doesn't work inside useEffect
const token = Cookies.get('authToken');
if (token) {
const decoded_token = jwtDecode(token);
if (decoded_token) {
console.debug('Here');
if (decoded_token.username === mod.author) {
setOwner(true);
}
}
loadItems();
}, []); // <-- Tells useEffect to run once after render
}
const base_page = (
<>
@ -86,7 +113,10 @@ function ModPage({name}) {
<div className={styles.content}>
<div className={styles.backgroundImage}></div>
<p className={styles.title}>{mod.display_name || mod.name }</p>
<p className={styles.fullDescription}>{mod.mod_infos.full_description || "No description"}</p>
<div className={styles.fullDescription}>
{/* Sanitized by the backend */}
<p dangerouslySetInnerHTML={{ __html: (mod.mod_infos.full_description || "No description") }} />
</div>
</div>
@ -137,6 +167,22 @@ function ModPage({name}) {
</a>
</div>
{ owner ? (
<div className={styles.panelActions}>
<Button
variant='delete'
className={styles.deleteButton}
onClick={handleDeleteMod}
>
Delete
</Button>
</div>
) : (
<> </>
)
}
</div>
</div>

View file

@ -7,7 +7,7 @@ export async function createMod(mod_data) {
try {
const auth_token = Cookies.get('authToken');
const response = await fetch(`${API_BASE_URL}/login`, {
const response = await fetch(`${API_BASE_URL}/mods`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
@ -17,6 +17,7 @@ export async function createMod(mod_data) {
});
if (!response.ok) {
console.error(response.body); //TODO integrate body to error
throw new Error(`HTTP error! status: ${response.status}`);
}
@ -24,7 +25,7 @@ export async function createMod(mod_data) {
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
console.error('Failed to create mod:', error);
throw error;
}
}
@ -39,7 +40,7 @@ export async function listMods(filters) {
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
console.error('Failed to fetch mods:', error);
throw error;
}
}
@ -54,7 +55,33 @@ export async function getMod(mod_name) {
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
console.error('Failed to fetch mod:', error);
throw error;
}
}
export async function deleteMod(mod_name) {
try {
const auth_token = Cookies.get('authToken');
const response = await fetch(`${API_BASE_URL}/mods/${mod_name}`, {
method: 'DELETE',
headers: {
'Authorization': auth_token,
}
});
if (!response.ok) {
console.error(response.body); //TODO integrate body to error
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to delete mod:', error);
throw error;
}
}

View file

@ -195,4 +195,24 @@
.version:hover {
background-color: #3a3a3a;
}
.panelActions {
position: absolute;
bottom: 0;
margin: 0 -2rem;
padding: 2rem;
width: 100%;
box-sizing: border-box;
}
/*TODO handle versions overlapping*/
.deleteButton {
width: 100%;
min-width: 5em;
font-size: 1.2rem;
}

View file

@ -54,6 +54,8 @@
font-size: 1.5em;
border-radius: .5em;
user-select: none;
transition: 200ms;
}