Functionnal register system, 404 page, splitted services into multiple files and various fixes

This commit is contained in:
Gu://em_ 2025-05-16 13:12:03 +02:00
parent 42a66ddda1
commit 93bf736ca9
23 changed files with 575 additions and 128 deletions

View file

@ -0,0 +1,10 @@
{
"port": 8100,
"backend": {
"address": "localhost",
"port": 8000,
"protocol": "http",
"path": "/"
}
}

View file

@ -7,6 +7,8 @@ import { Router } from 'preact-router';
// Pages // Pages
import HomePage from './pages/home'; import HomePage from './pages/home';
import AboutPage from './pages/about'; import AboutPage from './pages/about';
import NotFoundPage from './pages/not_found';
import ComingSoonPage from './pages/coming_soon';
import ModsPage from './pages/mods'; import ModsPage from './pages/mods';
import ModPage from './pages/mod_page' import ModPage from './pages/mod_page'
@ -48,6 +50,9 @@ export function App() {
<LoginPage path='/login'></LoginPage> <LoginPage path='/login'></LoginPage>
<RegisterPage path='/register'></RegisterPage> <RegisterPage path='/register'></RegisterPage>
<ComingSoonPage path='/soon'></ComingSoonPage>
<NotFoundPage path='/notfound' default ></NotFoundPage>
</Router> </Router>

View file

@ -0,0 +1,52 @@
import { h } from 'preact';
import { useState } from 'preact/hooks';
import { searchMods } from '../services/api'; // Your API fetching function
import styles from './SearchBar.module.css'; // Optional: CSS Modules
function SearchBar({ onResults }) {
const [searchTerm, setSearchTerm] = useState('');
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleInputChange = (event) => {
setSearchTerm(event.target.value);
};
const handleSearch = async () => {
if (!searchTerm.trim()) {
onResults([]); // Clear results if search term is empty
return;
}
setLoading(true);
setError(null);
try {
const results = await searchItems(searchTerm);
onResults(results); // Pass the fetched results to the parent component
} catch (err) {
setError('Failed to fetch search results.');
onResults([]); // Clear results on error
} finally {
setLoading(false);
}
};
return (
<div className={styles.searchBarContainer}>
<input
type="text"
placeholder="Search items..."
value={searchTerm}
onChange={handleInputChange}
className={styles.searchInput}
/>
<button onClick={handleSearch} className={styles.searchButton} disabled={loading}>
{loading ? 'Searching...' : 'Search'}
</button>
{error && <div className={styles.errorMessage}>{error}</div>}
</div>
);
}
export default SearchBar;

View file

@ -39,6 +39,9 @@
.leftItem:hover { .leftItem:hover {
background-color: #eaeaea10; background-color: #eaeaea10;
} }
.leftItem:active {
background-color: #eaeaea20;
}
/* WIP */ /* WIP */
.rightItem { .rightItem {

View file

@ -16,7 +16,7 @@ function AboutPage() {
return ( return (
<> <>
<a href='https://radio.oblic-parallels.fr' target="_blank"> <a href='/' target="_blank">
<img src={logo} class="logo img" alt="WF" /> <img src={logo} class="logo img" alt="WF" />
<p class="logo text"> radio </p> <p class="logo text"> radio </p>
</a> </a>

View file

@ -0,0 +1,29 @@
// Functions
import { h } from 'preact';
// Images
import logo from '../assets/logo.png'
// Styles
import '../styles/home.css'
// Components
import Button from '../components/Buttons/button';
function ComingSoonPage() {
return (
<>
<a href='/' target="_blank">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> radio </p>
</a>
<div class='title'>Coming soon</div>
<div class='background'></div>
<div class='halo'></div>
</>
);
}
export default ComingSoonPage;

View file

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

View file

@ -16,7 +16,7 @@ function HomePage() {
return ( return (
<> <>
<a href='https://radio.oblic-parallels.fr' target="_blank"> <a href='/' target="_blank">
<img src={logo} class="logo img" alt="WF" /> <img src={logo} class="logo img" alt="WF" />
<p class="logo text"> radio </p> <p class="logo text"> radio </p>
</a> </a>

View file

@ -2,6 +2,7 @@
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState } from 'preact/hooks';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
import { jwtDecode } from 'jwt-decode'
// Images // Images
import logo from '../assets/logo.png' import logo from '../assets/logo.png'
@ -14,7 +15,7 @@ import Button from '../components/Buttons/button';
import InputField from '../components/Fields/input_field'; import InputField from '../components/Fields/input_field';
// Functions // Functions
import { login } from '../services/api'; import { login } from '../services/auth';
function LoginPage() { function LoginPage() {
@ -41,8 +42,8 @@ function LoginPage() {
}; };
const handleSubmit = async (event) => { const handleSubmit = async (event) => {
event.preventDefault(); // Prevent the default form submission event.preventDefault();
setFieldErrors({ name: null, email: null, message: null }); setFieldErrors({ username: null, password: null});
setLoginStatus('logging in'); setLoginStatus('logging in');
@ -53,8 +54,12 @@ function LoginPage() {
setLoginStatus('success'); setLoginStatus('success');
if (response && response.token) { if (response && response.token) {
const decoded_token = jwtDecode(response.token);
if (!decoded_token) {
throw new Error("Couldn't decode token");
}
Cookies.set('authToken', response.token, { Cookies.set('authToken', response.token, {
expires: 30, expires: decoded_token.exp, //TODO not sure if it's the right value
path: '/', path: '/',
secure: true, // only send over https secure: true, // only send over https
sameSite: 'strict' sameSite: 'strict'

View file

@ -3,7 +3,7 @@ import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
// Functions // Functions
import { createMod } from '../services/api'; import { createMod } from '../services/mods';
// Components // Components
import InputField from '../components/Fields/input_field' import InputField from '../components/Fields/input_field'

View file

@ -3,7 +3,7 @@ import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
// Functions // Functions
import { fetchMod } from '../services/api'; import { getMod } from '../services/mods';
// Components // Components
@ -28,7 +28,7 @@ function ModPage({name}) {
setLoading(true); setLoading(true);
setError(false); setError(false);
try { try {
const fetched_mod = await fetchMod(name); const fetched_mod = await getMod(name);
setMod(fetched_mod); setMod(fetched_mod);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@ -40,22 +40,48 @@ function ModPage({name}) {
loadItems(); loadItems();
}, []); // <-- Tells useEffect to run once after render }, []); // <-- Tells useEffect to run once after render
const base_page = (
<>
<a href="/">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
</>
);
if (loading) { if (loading) {
// TODO replace by loading screen // TODO replace by loading screen
return <div>Loading mod</div> return (
<>
{base_page}
<div className={styles.container}>
<div className={styles.content}>
<p className={styles.title}>Loading...</p>
</div>
<div className={styles.infosPanel}> </div>
</div>
</>
);
} }
if (error) { if (error) {
// TODO replace by popup // TODO replace by popup
return <div>Couldn't load mod: {error}</div> return (
<>
{base_page}
<div className={styles.container}>
<div className={styles.content}>
<p className={styles.title}>Couldn't load this mod</p>
<p className={styles.fullDescription}>{error}</p>
</div>
<div className={styles.infosPanel}> </div>
</div>
</>
);
} }
return ( return (
<> <>
<a href="/"> {base_page}
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.content}> <div className={styles.content}>
<div className={styles.backgroundImage}></div> <div className={styles.backgroundImage}></div>

View file

@ -16,7 +16,7 @@ function ModpacksPage() {
return ( return (
<> <>
<a href='https://radio.oblic-parallels.fr' target="_blank"> <a href='/' target="_blank">
<img src={logo} class="logoSmall img" alt="WF" /> <img src={logo} class="logoSmall img" alt="WF" />
<p class="logoSmall text"> modpacks </p> <p class="logoSmall text"> modpacks </p>
</a> </a>

View file

@ -3,7 +3,7 @@ import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks'; import { useState, useEffect } from 'preact/hooks';
// Functions // Functions
import { fetchMods } from '../services/api'; import { listMods } from '../services/mods';
// Components // Components
import FiltersPanel from '../components/Filters/panel' import FiltersPanel from '../components/Filters/panel'
@ -29,7 +29,7 @@ function ModsPage() {
setLoading(true); setLoading(true);
setError(false); setError(false);
try { try {
const fetched_mods = await fetchMods(); const fetched_mods = await listMods();
setMods(fetched_mods); setMods(fetched_mods);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@ -41,22 +41,49 @@ function ModsPage() {
loadItems(); loadItems();
}, []); // <-- Tells useEffect to run once after render }, []); // <-- Tells useEffect to run once after render
// Base page
const base_page = (
<>
<a href="/" target="_blank">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
<FiltersPanel></FiltersPanel>
</>
);
if (loading) { if (loading) {
// TODO replace by loading screen // TODO replace by loading screen
return <div>Loading mods</div> return (
<>
{base_page}
<div class='content-container'>
<div class='loadingContent'>
Loading mods
</div>
</div>
</>
);
} }
if (error) { if (error) {
// TODO replace by popup // TODO replace by popup
return <div>Couldn't load mods: {error}</div> return (
<>
{base_page}
<div class='content-container'>
<div class='loadingContent'>
Couldn't load mods: {error}
</div>
</div>
</>
);
} }
return ( return (
<> <>
<a href="https://radio.oblic-parallels.fr" target="_blank"> {base_page}
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
<FiltersPanel></FiltersPanel>
<div class='content-container'> <div class='content-container'>
{/* <GridCard>Test card</GridCard> */} {/* <GridCard>Test card</GridCard> */}
{/* <GridCard/> */} {/* <GridCard/> */}

View file

@ -0,0 +1,30 @@
// Functions
import { h } from 'preact';
// Images
import logo from '../assets/logo.png'
// Styles
import '../styles/home.css'
// Components
import Button from '../components/Buttons/button';
function NotFoundPage() {
return (
<>
<a href='/' target="_blank">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> radio </p>
</a>
<div class='bigTitle'>404</div>
<div class='subtitle'>Page not found</div>
<div class='background'></div>
<div class='halo'></div>
</>
);
}
export default NotFoundPage;

View file

@ -1,8 +1,12 @@
// Functions // Preact
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks';
import Cookies from 'js-cookie';
import { jwtDecode } from 'jwt-decode'
// Images // Images
import logo from '../assets/logo.png' import Logo from '../assets/logo.png'
import Account from '../assets/account.svg'
// Styles // Styles
import styles from '../styles/login.module.css' import styles from '../styles/login.module.css'
@ -11,31 +15,104 @@ import styles from '../styles/login.module.css'
import Button from '../components/Buttons/button'; import Button from '../components/Buttons/button';
import InputField from '../components/Fields/input_field'; import InputField from '../components/Fields/input_field';
// Functions
import { register } from '../services/auth';
function RegisterPage() { function RegisterPage() {
const username_input= ""; // useState
const display_name_input= ""; const [username, setUsername] = useState('');
const email_input= ""; const [display_name, setDisplayName] = useState('');
const password_input= ""; const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [registerStatus, setRegisterStatus] = useState(null); // To track success/error
const [fieldErrors, setFieldErrors] = useState({
username: null,
display_name: null,
email: null,
password: null
});
const handleUsernameChange = (event) => {
setUsername(event.target.value);
// Reset error
setFieldErrors(prevErrors => ({ ...prevErrors, username: null }));
};
const handlePasswordChange = (event) => {
setPassword(event.target.value);
// Reset error
setFieldErrors(prevErrors => ({ ...prevErrors, password: null }));
};
const handleEmailChange = (event) => {
setEmail(event.target.value);
// Reset error
setFieldErrors(prevErrors => ({ ...prevErrors, email: null }));
};
const handleDisplayNameChange = (event) => {
setDisplayName(event.target.value);
// Reset error
setFieldErrors(prevErrors => ({ ...prevErrors, display_name: null }));
};
const handleSubmit = async (event) => {
event.preventDefault();
setFieldErrors({ username: null, password: null, email: null, display_name: null});
setRegisterStatus('registering');
try {
const response = await register({username: username,
display_name: display_name,
email: email,
password: password});
console.debug('Registered successfully:', response);
setRegisterStatus('success');
window.location.replace("/login");
//TODO success screen
} catch (error) {
console.error('Register failed:', error);
setRegisterStatus('error');
//TODO handle different codes differently
setFieldErrors({
username: ' ',
email: ' ',
display_name: ' ',
password: 'Error'
});
}
finally {
setRegisterStatus(null);
}
};
return ( return (
<> <>
<a href='https://radio.oblic-parallels.fr' target="_blank"> <a href='/'>
<img src={logo} class="logo img" alt="WF" /> <img src={Logo} class="logo img" alt="WF" />
<a class="logo text"> radio </a> <p class="logo text"> radio </p>
</a> </a>
<div className={styles.container}> <div className={styles.container}>
<img src={logo} className={styles.loginImage}></img> <form onSubmit={handleSubmit}>
<form> <img src={Account} className={styles.loginImage} style={'cursor: pointer'}></img>
{/* <input type="file" id="profile_picture" style="display: none;"></input> */}
<div className={styles.fieldsContainer}> <div className={styles.fieldsContainer}>
{/* username */} {/* username */}
<InputField <InputField
id="username" id="username"
name="username" name="username"
value={username_input} value={username}
// onChange={handleNameChange} onChange={handleUsernameChange}
// error={nameError} error={fieldErrors.username}
placeholder="username" placeholder="username"
required required
> >
@ -45,9 +122,9 @@ function RegisterPage() {
<InputField <InputField
id="display_name" id="display_name"
name="display_name" name="display_name"
value={display_name_input} value={display_name}
// onChange={handleNameChange} onChange={handleDisplayNameChange}
// error={nameError} error={fieldErrors.display_name}
placeholder="display_name" placeholder="display_name"
required required
> >
@ -57,9 +134,9 @@ function RegisterPage() {
<InputField <InputField
id="email" id="email"
name="email" name="email"
value={email_input} value={email}
// onChange={handleNameChange} onChange={handleEmailChange}
// error={nameError} error={fieldErrors.email}
placeholder="email" placeholder="email"
required required
> >
@ -69,18 +146,22 @@ function RegisterPage() {
<InputField <InputField
id="password" id="password"
name="password" name="password"
value={password_input} value={password}
type="password" type="password"
// onChange={handleNameChange} onChange={handlePasswordChange}
// error={nameError} error={fieldErrors.password}
placeholder="password" placeholder="password"
required required
> >
</InputField> </InputField>
</div> </div>
<div className={styles.buttonsContainer}> <div className={styles.buttonsContainer}>
<Button className={styles.loginButton}> <Button
Register className={styles.loginButton}
type='submit'
disabled={registerStatus === 'registering'}
>
{registerStatus === 'registering' ? 'Registering' : registerStatus === 'success' ? 'Success' : 'Register'}
</Button> </Button>
</div> </div>
</form> </form>

View file

@ -44,20 +44,22 @@ function SettingsPage() {
return ( return (
<> <>
<a href='/'> <a href='/'>
<img src={Logo} class="logoSmall img" alt="WF" /> <img src={Logo} className={styles.logo} alt="WF" />
<p class="logoSmall text"> settings </p> <p className={styles.logoTitle}> settings </p>
</a> </a>
<div className={styles.tabsContainer}> <div className={styles.tabsContainer}>
<a href='/settings'> <a href='/settings'>
<p className={styles.tab}>Global</p> <p className={styles.tab}>Global</p>
</a> </a>
<a href='/notfound'>
<p className={styles.tab}>User</p>
</a>
{user ? ( {user ? (
<a onClick={() => {logout()}}> <>
<p className={`${styles.tab} ${styles.logout}`}>Logout</p> <a href='/notfound'>
</a> <p className={styles.tab}>User</p>
</a>
<a onClick={() => {logout()}}>
<p className={`${styles.tab} ${styles.logout}`}>Logout</p>
</a>
</>
) : ) :
'' ''
} }

View file

@ -1,66 +0,0 @@
const API_BASE_URL = 'http://localhost:8000';
export async function fetchMods() {
try {
const response = await fetch(`${API_BASE_URL}/list/mods`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
return [];
}
}
export async function fetchMod(mod_name) {
try {
const response = await fetch(`${API_BASE_URL}/mods/${mod_name}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
return null;
}
}
export async function login(username, password) {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: username, password: password })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// console.error('Failed to fetch items:', error);
throw error;
}
}
export async function createMod(username, password) {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
body: JSON.stringify({ }) //TODO
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
return null;
}
}

View file

@ -0,0 +1,46 @@
import Cookies from 'js-cookie';
const API_BASE_URL = 'http://localhost:8000'; //TODO use config manager instead
export async function login(username, password) {
try {
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ username: username, password: password })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// console.error('Failed to fetch items:', error);
throw error;
}
}
export async function register(user_data) {
try {
console.debug(user_data);
console.debug(JSON.stringify(user_data));
const response = await fetch(`${API_BASE_URL}/users`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(user_data)
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
// console.error('Failed to fetch items:', error);
throw error;
}
}

View file

@ -0,0 +1,70 @@
// --- Define constants ---
// Imports
import fs from "fs";
import path from "path";
import { version } from "../../package.json";
// Var decalaration
const config_folder = "config";
const config_file_name = "config.json"
// Global variables
let config = {};
let backend_url = "";
// --- Default config ---
const default_config = {
"port": 8100,
"backend": {
"address": "localhost",
"port": 8000,
"protocol": "http",
"path": "/"
}
}
function loadConfig() {
let user_config;
// Parse
try {
// Get user config
user_config = JSON.parse(fs.readFileSync(path.resolve(path.join(config_folder, config_file_name))));
// Merge default and user configs (default values)
config = { ...default_config, ...user_config };
backend_url = config.backend.protocol + '://'
+ config.backend.address + ':'
+ config.backend.port
+ config.backend.path;
//TODO test url
}
catch (err) {
// Error messages
console.debug("Error:", err)
console.error("Error loading configuration, using the default settings");
console.debug("Search path:", path.resolve("./"));
console.debug("Config file:", path.resolve(path.join(config_folder, config_file_name)))
config = default_config;
}
return config;
}
function getBackendUrl() {
if (backend_url) {
return backend_url;
} else {
throw new Error("Backend url is not valid");
}
}

View file

@ -0,0 +1,60 @@
import Cookies from 'js-cookie';
const API_BASE_URL = 'http://localhost:8000'; //TODO use config manager instead
export async function createMod(mod_data) {
try {
const auth_token = Cookies.get('authToken');
const response = await fetch(`${API_BASE_URL}/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': auth_token,
},
body: JSON.stringify({...mod_data})
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
throw error;
}
}
export async function listMods() {
try {
const response = await fetch(`${API_BASE_URL}/list/mods`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
throw error;
}
}
export async function getMod(mod_name) {
try {
const response = await fetch(`${API_BASE_URL}/mods/${mod_name}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return data;
} catch (error) {
console.error('Failed to fetch items:', error);
throw error;
}
}

View file

@ -139,6 +139,10 @@ h1 {
} }
.loadingContent {
color: #7a7a7a;
}
/* Page transitions */ /* Page transitions */
.page { .page {

View file

@ -32,6 +32,38 @@
letter-spacing: 0.1rem; letter-spacing: 0.1rem;
} }
.bigTitle {
position: fixed;
top: 26rem;
left: 50%;
transform: translate(-50%, -50%); /*Center (compensate size)*/
z-index: -2;
color: #eaeaea;
text-align: center;
font-family: "Inter";
font-weight: 600;
font-size: 8rem;
user-select: none;
letter-spacing: 0.1rem;
}
.subtitle {
position: fixed;
top: 34rem;
left: 50%;
transform: translate(-50%, -50%); /*Center (compensate size)*/
z-index: -2;
color: #eaeaea;
text-align: center;
font-family: "Inter";
font-weight: 600;
font-size: 500%;
user-select: none;
letter-spacing: 0.1rem;
}
.start-button { .start-button {
position: fixed; position: fixed;
top: 48rem; top: 48rem;

View file

@ -1,3 +1,29 @@
.logo {
position: absolute;
top: 1.6rem;
left: 2rem;
height: 6em;
padding: 1.5em;
user-select: none;
}
.logoTitle {
position: absolute;
left: 10rem;
top: 4rem;
margin: 0;
color: #eaeaea;
font-family: Inter;
font-weight: 600;
font-size: 2.5em;
user-select: none;
}
.tabsContainer { .tabsContainer {
position: absolute; position: absolute;
left: 4rem; left: 4rem;
@ -32,7 +58,11 @@
} }
.tab:hover { .tab:hover {
background-color: #3a3a3a; background-color: #eaeaea20;
}
.tab:active {
background-color: #eaeaea30;
} }
.logout { .logout {