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": { "auth": {
"JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY", "JWT_secret": "HGF7654EGBNKJNBJH6754356788GJHGY",
"tokenExpiry": "1h" "tokenExpiry": "30d"
} }
} }

View file

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

View file

@ -1,6 +1,8 @@
// Preact // Preact
import { h } from 'preact'; import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode'
// Functions // Functions
import { createMod } from '../services/mods'; import { createMod } from '../services/mods';
@ -18,12 +20,112 @@ import styles from '../styles/content_creation.module.css'
import Button from '../components/Buttons/button'; import Button from '../components/Buttons/button';
function ModCreationPage() { function ModCreationPage() {
//TODO add missing fields
const user = "guill" //TODO const null_fields = {
name: null,
const mod_infos = { display_name: null,
name: "" 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 ( return (
<> <>
@ -33,7 +135,7 @@ function ModCreationPage() {
</a> </a>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.form}> <form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.topFields}> <div className={styles.topFields}>
<div> <div>
@ -41,9 +143,9 @@ function ModCreationPage() {
<InputField <InputField
id="name" id="name"
name="name" name="name"
value={mod_infos.name} value={name}
// onChange={handleNameChange} onChange={handleNameChange}
// error={nameError} error={field_errors.name}
placeholder="name" placeholder="name"
required required
className={styles.smallField} className={styles.smallField}
@ -54,9 +156,9 @@ function ModCreationPage() {
<InputField <InputField
id="display_name" id="display_name"
name="display_name" name="display_name"
value={mod_infos.display_name} value={display_name}
// onChange={handleNameChange} onChange={handleDisplayNameChange}
// error={nameError} error={field_errors.display_name}
placeholder="display_name" placeholder="display_name"
className={styles.smallField} className={styles.smallField}
> >
@ -67,9 +169,9 @@ function ModCreationPage() {
<TextArea <TextArea
id="description" id="description"
name="description" name="description"
value={mod_infos.display_name} value={description}
// onChange={handleNameChange} onChange={handleDescriptionChange}
// error={nameError} error={field_errors.description}
placeholder="description" placeholder="description"
containerClass={styles.descriptionField} containerClass={styles.descriptionField}
> >
@ -81,9 +183,9 @@ function ModCreationPage() {
<TextArea <TextArea
id="full_description" id="full_description"
name="full_description" name="full_description"
value={mod_infos.display_name} value={full_description}
// onChange={handleNameChange} onChange={handleFullDescriptionChange}
// error={nameError} error={field_errors.mod_infos.full_description}
placeholder="full_description" placeholder="full_description"
containerClass={styles.fullDescriptionField} containerClass={styles.fullDescriptionField}
> >
@ -92,17 +194,15 @@ function ModCreationPage() {
</div> </div>
<Button className={styles.createButton}> <Button className={styles.createButton}>
Create Publish
</Button> </Button>
</div> </form>
<div className={styles.infosPanel}> <div className={styles.infosPanel}>
<a href={"/users/" + user}>
<img src={profile} className={styles.profilePicture}></img> <img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{user}</p> <p className={styles.author}>{user}</p>
</a>
</div> </div>
</div> </div>

View file

@ -1,11 +1,14 @@
// Preact // Preact
import { h } from 'preact'; import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode'
// Functions // Functions
import { getMod } from '../services/mods'; import { getMod, deleteMod } from '../services/mods';
// Components // Components
import Button from '../components/Buttons/button';
// Images // Images
import logo from '../assets/logo.png' import logo from '../assets/logo.png'
@ -22,8 +25,11 @@ function ModPage({name}) {
const [mod, setMod] = useState({}) const [mod, setMod] = useState({})
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
const [owner, setOwner] = useState(false);
// UseEffect // UseEffect
useEffect(() => { useEffect(() => {
// Load mod informations
async function loadItems() { async function loadItems() {
setLoading(true); setLoading(true);
setError(false); setError(false);
@ -38,8 +44,29 @@ function ModPage({name}) {
} }
loadItems(); loadItems();
}, []); // <-- Tells useEffect to run once after render }, []); // <-- 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);
}
}
}
const base_page = ( const base_page = (
<> <>
<a href="/"> <a href="/">
@ -86,7 +113,10 @@ function ModPage({name}) {
<div className={styles.content}> <div className={styles.content}>
<div className={styles.backgroundImage}></div> <div className={styles.backgroundImage}></div>
<p className={styles.title}>{mod.display_name || mod.name }</p> <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> </div>
@ -137,6 +167,22 @@ function ModPage({name}) {
</a> </a>
</div> </div>
{ owner ? (
<div className={styles.panelActions}>
<Button
variant='delete'
className={styles.deleteButton}
onClick={handleDeleteMod}
>
Delete
</Button>
</div>
) : (
<> </>
)
}
</div> </div>
</div> </div>

View file

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

View file

@ -196,3 +196,23 @@
.version:hover { .version:hover {
background-color: #3a3a3a; 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; font-size: 1.5em;
border-radius: .5em; border-radius: .5em;
user-select: none;
transition: 200ms; transition: 200ms;
} }