Backend and frontend filtering (search and author), dashboard now displays creations

This commit is contained in:
Gu://em_ 2025-05-17 16:33:49 +02:00
parent 58268a94a9
commit 683f8784a7
11 changed files with 189 additions and 56 deletions

View file

@ -5,7 +5,8 @@ const { authorizeModModification, authenticateToken } = require("../middleware/a
async function listMods(req, res) { async function listMods(req, res) {
try { try {
// Query // Query
const query_result = await mod_service.getAllMods(); const filters = req.query;
const query_result = await mod_service.listMods(filters);
res.json(query_result); res.json(query_result);
} catch (error) { } catch (error) {
handleError(error, res); handleError(error, res);

View file

@ -28,7 +28,7 @@ class SQLiteDatabase {
// Runs with a result (ex: SELECT) // Runs with a result (ex: SELECT)
async query(sql, params = []) { async query(sql, params = []) {
try { try {
if (params.length > 0) { if ( params != []) {
return this.db.prepare(sql).all(params); return this.db.prepare(sql).all(params);
} else { } else {
return this.db.prepare(sql).all(); return this.db.prepare(sql).all();

View file

@ -5,12 +5,21 @@ const db = getDatabase();
// --- Get --- // --- Get ---
async function getAllMods() { async function listMods(filters) {
return await db.query("SELECT name, display_name, author, description FROM Mods"); console.debug(filters);
return await db.query(`SELECT name, display_name, author, description FROM Mods
WHERE
(CASE WHEN @search IS NOT NULL THEN
name LIKE '%' || @search || '%' OR
display_name LIKE '%' || @search || '%' OR
description LIKE '%' || @search || '%'
ELSE TRUE END) AND
(CASE WHEN @author IS NOT NULL THEN author = @author ELSE TRUE END);
`, filters);
} }
async function getModByName(name) { async function getModByName(name) {
return await db.query("SELECT name, display_name, author FROM Mods WHERE name = ?;", [name]); return await db.query("SELECT name, display_name, author FROM Mods WHERE name = @name;", {name: name});
} }
@ -185,7 +194,7 @@ async function containsTag(name, tag) {
// --- Exports --- // --- Exports ---
module.exports = { getAllMods, getModByName, getFullModInfos, module.exports = { listMods, getModByName, getFullModInfos,
listVersions, listTags, getVersionByNumber, getVersion, listVersions, listTags, getVersionByNumber, getVersion,
createMod, addVersion, addTags, createMod, addVersion, addTags,
updateMod, updateMod,

View file

@ -7,8 +7,14 @@ const { sanitizeModData } = require("../utils/sanitize");
// --- Get --- // --- Get ---
async function getAllMods() { async function listMods(filters) {
return model.getAllMods(); //TODO Validate filters
console.warn("Skipping full filters validation: Not implemented");
filters.author = filters.author || null;
filters.search = filters.search || null;
return await model.listMods({...filters});
} }
async function getModByName(name) { async function getModByName(name) {
@ -162,7 +168,7 @@ async function deleteTags(mod, tags) {
return { "mod": mod, "tags": res}; return { "mod": mod, "tags": res};
} }
module.exports = { getAllMods, getModByName, getFullModInfos, module.exports = { listMods, getModByName, getFullModInfos,
createMod, addTags, addVersion, createMod, addTags, addVersion,
updateMod, updateMod,
deleteMod, deleteTags, deleteVersion }; deleteMod, deleteTags, deleteVersion };

View file

@ -17,14 +17,14 @@ function SearchBar({ onSearch }) {
const handleInputChange = (event) => { const handleInputChange = (event) => {
const new_search_input = event.target.value; const new_search_input = event.target.value;
setSearchTerm(new_search_input); setSearchInput(new_search_input);
if (timeout_id.current) { if (timeout_id.current) {
clearTimeout(timeout_id.current); clearTimeout(timeout_id.current);
} }
timeout_id.current = setTimeout(() => { timeout_id.current = setTimeout(() => {
onSearch(newSearchTerm); onSearch(new_search_input);
}, 500); }, 500);
}; };
@ -43,7 +43,7 @@ function SearchBar({ onSearch }) {
type="text" type="text"
placeholder="Search" placeholder="Search"
value={search_input} value={search_input}
onChange={handleInputChange} onInput={handleInputChange}
className={styles.searchBarInput} className={styles.searchBarInput}
/> />
</div> </div>

View file

@ -0,0 +1 @@
//TODO

View file

@ -1,5 +1,11 @@
// Preact
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode';
// Functions
import { listMods } from '../services/mods';
// Components // Components
import Button from '../components/Buttons/button' import Button from '../components/Buttons/button'
@ -13,17 +19,98 @@ import styles from '../styles/dashboard.module.css'
function DashboardPage() { function DashboardPage() {
// useStates
const [creations, setCreations] = useState([]);
const [loading_creations, setLoadingCreations] = useState(true)
const [creations_error, setCreationsError] = useState(null)
// Functions
let user;
async function loadUserInformations() {
//TODO use a service
const token = Cookies.get('authToken');
if (token) {
const decoded_token = jwtDecode(token);
if (decoded_token) {
user = decoded_token.username
}
}
if (!user) {
console.error("Cannot retrieve user from token");
location.replace('/login')
}
}
async function loadCreations() {
setLoadingCreations(true);
setCreationsError(false);
try {
const fetched_items = await listMods({author: user});
setCreations(fetched_items);
} catch (err) {
setCreationsError(err.message);
} finally {
setLoadingCreations(false);
}
}
// useEffects
useEffect(async ()=> {
await loadUserInformations();
await loadCreations();
}, []);
// Handles
const handleCreate = () => { const handleCreate = () => {
window.location.href('/create/mod'); window.location.href('/create/mod');
} }
// Page
// Base page
const base_page = (
<a href='/' target="_blank">
<img src={Logo} class="logoSmall img" alt="WF" />
<p class="logoSmall text"> dashboard </p>
</a>
);
if (loading_creations) {
// TODO replace by icons
return (
<>
{base_page}
<div class='container'>
<p>Loading</p>
</div>
</>
);
}
if (creations_error) {
// TODO replace by popup
return (
<>
{base_page}
<div class='container'>
<p>Error: {creations_error}</p>
</div>
</>
);
}
return ( return (
<> <>
<a href='/' target="_blank"> {base_page}
<img src={Logo} class="logoSmall img" alt="WF" />
<p class="logoSmall text"> dashboard </p>
</a>
<div class='container'> <div class='container'>
<div className={styles.category}> <div className={styles.category}>
<p className={styles.title}> <p className={styles.title}>
@ -42,6 +129,17 @@ function DashboardPage() {
Your creations Your creations
</p> </p>
<div className={styles.tiles}> <div className={styles.tiles}>
{/* Temporary, missing card component */}
{creations.map( (item) => {
console.debug(item.name);
return (<div className={styles.emptyTile}>
<div className={styles.emptyTileText}>
{item.display_name}
</div>
</div>);
})}
<a className={styles.emptyTile} href='/create/mod'> <a className={styles.emptyTile} href='/create/mod'>
<p className={styles.emptyTileText}> <p className={styles.emptyTileText}>
<img src={Add} ></img> <img src={Add} ></img>

View file

@ -60,7 +60,6 @@ function ModPage({name}) {
if (token) { if (token) {
const decoded_token = jwtDecode(token); const decoded_token = jwtDecode(token);
if (decoded_token) { if (decoded_token) {
console.debug('Here');
if (decoded_token.username === mod.author) { if (decoded_token.username === mod.author) {
setOwner(true); setOwner(true);
} }

View file

@ -31,30 +31,32 @@ function ModsPage() {
setSearchInput(new_search_input); setSearchInput(new_search_input);
}; };
// UseEffect
useEffect(() => {
async function loadItems() { async function loadItems() {
setLoading(true); setLoading(true);
setError(false); setError(false);
try { try {
const filters = { const filters = {
search: search_input, search: search_input,
categories: selected_categories };
}; console.debug("Searching", search_input);
const fetched_mods = await listMods(filters); const fetched_mods = await listMods(filters);
setMods(fetched_mods); setMods(fetched_mods);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
} finally { } finally {
setLoading(false); setLoading(false);
}
} }
}
// useEffect
useEffect(() => {
loadItems(); loadItems();
}, []); // <-- Tells useEffect to run once after render }, [search_input]);
// Base page // Base page
const base_page = ( const base_page = (

View file

@ -32,31 +32,48 @@ export async function createMod(mod_data) {
export async function listMods(filters) { export async function listMods(filters) {
try { try {
const response = await fetch(`${API_BASE_URL}/list/mods`); let url_parameters = "";
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); // Parse filters
} for (const [key, value] of Object.entries(filters)) {
const data = await response.json(); if (url_parameters === "") {
return data; url_parameters += "?"
} else {
url_parameters += "&"
}
url_parameters += `${key}=${value}`;
}
// Query
const response = await fetch(`${API_BASE_URL}/list/mods/${url_parameters}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
// Return
const data = await response.json();
return data;
} catch (error) { } catch (error) {
console.error('Failed to fetch mods:', error); console.error('Failed to fetch mods:', error);
throw error; throw error;
} }
} }
export async function getMod(mod_name) { export async function getMod(mod_name) {
try { try {
const response = await fetch(`${API_BASE_URL}/mods/${mod_name}`); const response = await fetch(`${API_BASE_URL}/mods/${mod_name}`);
if (!response.ok) { if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`); throw new Error(`HTTP error! status: ${response.status}`);
} }
const data = await response.json(); const data = await response.json();
return data; return data;
} catch (error) { } catch (error) {
console.error('Failed to fetch mod:', error); console.error('Failed to fetch mod:', error);
throw error; throw error;
} }
} }