Mod, createMod and About pages, input fields, some more content and a LOT of fixes (including backend)

This commit is contained in:
Gu://em_ 2025-05-15 01:20:44 +02:00
parent a7cf958770
commit 534d251852
28 changed files with 909 additions and 52 deletions

View file

@ -43,7 +43,7 @@ async function getModByName(req, res) {
try { try {
// Query // Query
const name = req.params.name const name = req.params.name
const query_result = await mod_service.getModByName(name); const query_result = await mod_service.getFullModInfos(name);
res.json(query_result); res.json(query_result);
} catch (error) { } catch (error) {
handleError(error, res); handleError(error, res);

View file

@ -14,24 +14,25 @@ async function getModByName(name) {
} }
async function getModFullInfos(name) { async function getFullModInfos(name) {
// Query // Query
const base_infos = db.query(`SELECT * FROM Mods WHERE name = ?`, [name]); const base_infos = db.query(`SELECT * FROM Mods WHERE name = ?`, [name]);
const other_infos = db.query(`SELECT full_description, license, links, creation_date, downloads_count const other_infos = db.query(`SELECT full_description, license, links, creation_date, downloads_count
FROM ModInfos WHERE name = ?`, [name]); FROM ModInfos WHERE mod = ?`, [name]);
const tags = getModTags(name); const tags = listTags(name);
// Merge return [await base_infos, await other_infos, await tags];
const res = {...await base_infos, ...await other_infos, ...tags};
return res;
} }
async function listVersions(mod_name) { async function listVersions(mod_name) {
return await db.query("SELECT * FROM ModVersions WHERE mod = ?", [mod_name]); return await db.query("SELECT * FROM ModVersions WHERE mod = ?", [mod_name]);
} }
async function listTags(mod_name) {
return await db.query(`SELECT tag FROM ModTags WHERE mod = ?`, [mod_name]);
}
async function getVersionByNumber(mod_name, version_number) { async function getVersionByNumber(mod_name, version_number) {
return await db.query(`SELECT * FROM ModVersions return await db.query(`SELECT * FROM ModVersions
WHERE mod = ? WHERE mod = ?
@ -184,8 +185,8 @@ async function containsTag(name, tag) {
// --- Exports --- // --- Exports ---
module.exports = { getAllMods, getModByName, getModFullInfos, module.exports = { getAllMods, getModByName, getFullModInfos,
listVersions, getVersionByNumber, getVersion, listVersions, listTags, getVersionByNumber, getVersion,
createMod, addVersion, addTags, createMod, addVersion, addTags,
updateMod, updateMod,
deleteMod, deleteVersion, deleteTags, deleteMod, deleteVersion, deleteTags,

View file

@ -12,7 +12,7 @@ async function getAllMods() {
} }
async function getModByName(name) { async function getModByName(name) {
const res = model.getModByName(name); const res = await model.getModByName(name);
if (res.length == 0) { if (res.length == 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found"); throw new AppError(404, "Cannot find mod with this name", "Not found");
} }
@ -20,16 +20,20 @@ async function getModByName(name) {
} }
async function getFullModInfos(name) { async function getFullModInfos(name) {
const res = model.getFullModInfos(name); const [base_infos, other_infos, tags] = await model.getFullModInfos(name);
if (res.length == 0) { // Check
throw new AppError(404, "Cannot find mod with this name", "Not found"); if (base_infos.length == 0 || other_infos.length === 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found", "Couldn't retrieve from database correctly");
} }
return res[0]; // Merge
const mod_infos = {...other_infos[0], tags: tags}
const res = {...base_infos[0], mod_infos: mod_infos};
return res;
} }
async function getModVersion(infos) { async function getModVersion(infos) {
const { mod, version_number, game_version, platform, environment} = infos; const { mod, version_number, game_version, platform, environment} = infos;
const res = model.getModVersion(mod, version_number, game_version, platform, environment); const res = await model.getModVersion(mod, version_number, game_version, platform, environment);
if (res.length == 0) { if (res.length == 0) {
throw new AppError(404, "Cannot find mod with this name", "Not found"); throw new AppError(404, "Cannot find mod with this name", "Not found");
} }
@ -51,7 +55,7 @@ async function createMod(mod_data, author) {
mod_infos.full_description = await mdToHtml(mod_infos.full_description); // Convert mod_infos.full_description = await mdToHtml(mod_infos.full_description); // Convert
await sanitizeModData(mod_data); // Sanitize await sanitizeModData(mod_data); // Sanitize
//TODO //TODO
mod_infos.creation_date = 0 mod_infos.creation_date = Date.now();
// Write changes to database // Write changes to database
await model.createMod(name, display_name, author, description, mod_infos); await model.createMod(name, display_name, author, description, mod_infos);

View file

@ -7,8 +7,11 @@ import { Router } from 'preact-router';
// Pages // Pages
import HomePage from './pages/home'; import HomePage from './pages/home';
import ModsPage from './pages/mods'; import ModsPage from './pages/mods';
// import AboutPage from './pages/about'; import ModpacksPage from './pages/modpacks';
import AboutPage from './pages/about';
import SettingsPage from './pages/settings'; import SettingsPage from './pages/settings';
import ModPage from './pages/mod_page'
import ModCreationPage from './pages/mod_creation'
// Components // Components
import NavBar from './components/NavBar/navbar' import NavBar from './components/NavBar/navbar'
@ -30,8 +33,12 @@ export function App() {
<Router> <Router>
<HomePage path="/" /> <HomePage path="/" />
<ModsPage path="/mods" /> <ModsPage path="/mods" />
{/* <AboutPage path="/about" /> */} <ModpacksPage path="/modpacks" />
<AboutPage path="/about" />
<SettingsPage path="/settings" /> <SettingsPage path="/settings" />
<ModPage path="/mods/:name"></ModPage>
<ModCreationPage path="/create/mod" ></ModCreationPage>
</Router> </Router>

Binary file not shown.

After

Width:  |  Height:  |  Size: 439 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m480-147.34-35.9-32.51q-101.87-93.03-168.34-160.06-66.48-67.04-105.59-119.1-39.12-52.07-54.64-94.34Q100-595.63 100-638.46q0-83.25 56.14-139.39Q212.28-834 295.13-834q54.69 0 102.05 26.94 47.36 26.93 82.82 77.99 38.44-52.54 84.89-78.74Q611.34-834 664.87-834q82.85 0 138.99 56.15Q860-721.71 860-638.46q0 42.83-15.53 85.11-15.52 42.27-54.61 94.28-39.08 52.01-105.52 119.1T515.9-179.85L480-147.34Zm0-67.12q98.48-89.65 162.08-153.68 63.6-64.03 100.89-111.79 37.29-47.76 52.03-84.89 14.74-37.13 14.74-73.55 0-62.81-41.07-104.09-41.08-41.28-103.65-41.28-49.89 0-91.88 29.32-41.99 29.32-71.14 85.09h-44.41q-29.13-55.49-71.05-84.95t-91.56-29.46q-62.19 0-103.45 41.28-41.27 41.28-41.27 104.29 0 36.38 14.78 73.64 14.79 37.27 51.9 85.19 37.11 47.93 101.14 111.66Q382.1-303.95 480-214.46Zm0-284.64Z"/></svg>

After

Width:  |  Height:  |  Size: 910 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M232-253.08q59.92-41.3 119.23-64.03 59.31-22.74 128.77-22.74 69.46 0 129.08 22.74 59.61 22.73 119.53 64.03 43.62-50.53 64.81-106.72 21.19-56.19 21.19-120.2 0-141.54-96.53-238.08-96.54-96.53-238.08-96.53-141.54 0-238.08 96.53-96.53 96.54-96.53 238.08 0 64.01 21.5 120.2T232-253.08Zm247.78-204.23q-53.93 0-90.74-37.02-36.81-37.03-36.81-90.96 0-53.94 37.03-90.75 37.02-36.81 90.96-36.81 53.93 0 90.74 37.03 36.81 37.02 36.81 90.96 0 53.94-37.03 90.74-37.02 36.81-90.96 36.81Zm.69 357.31q-79.01 0-148.24-29.77-69.24-29.77-120.96-81.58-51.73-51.8-81.5-120.72-29.77-68.92-29.77-148T129.77-628q29.77-68.85 81.58-120.65 51.8-51.81 120.72-81.58 68.92-29.77 148-29.77T628-830.23q68.85 29.77 120.65 81.58 51.81 51.8 81.58 120.68Q860-559.09 860-480.47q0 79.01-29.77 148.24-29.77 69.24-81.58 120.96-51.8 51.73-120.68 81.5Q559.09-100 480.47-100Zm-.47-45.39q55.77 0 110-17.73t102.15-57.34q-47.92-35.23-101.5-54.62-53.57-19.38-110.65-19.38-57.08 0-110.85 19.19-53.77 19.19-100.92 54.81 47.54 39.61 101.77 57.34 54.23 17.73 110 17.73Zm.05-357.3q35.87 0 59.1-23.29 23.24-23.28 23.24-59.15t-23.29-59.1q-23.28-23.23-59.15-23.23t-59.1 23.28q-23.24 23.29-23.24 59.16t23.29 59.1q23.28 23.23 59.15 23.23Zm-.05-82.39Zm0 365.16Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -1,11 +1,12 @@
import { h } from 'preact' // Necessary ? import { h } from 'preact' // Necessary ?
import styles from './button.module.css' import styles from './button.module.css'
function Button({ children, onClick, className, variant = 'primary', ...rest}) { function Button({ children, onClick, href, className, variant = 'primary', ...rest}) {
const buttonClasses = `${styles.button} ${styles[variant] || ''} ${className || ''}` const buttonClasses = `${styles.button} ${styles[variant] || ''} ${className || ''}`
return ( return (
<a href={href}>
<button <button
className={buttonClasses} className={buttonClasses}
onClick= {onClick} onClick= {onClick}
@ -13,6 +14,7 @@ function Button({ children, onClick, className, variant = 'primary', ...rest}) {
> >
{children} {children}
</button> </button>
</a>
) )
} }

View file

@ -13,6 +13,10 @@
transition: 300ms; transition: 300ms;
} }
.button:hover {
cursor: pointer;
}
.primary { .primary {
background-color: #353be5; background-color: #353be5;
border-color: #4e53dc; border-color: #4e53dc;

View file

@ -0,0 +1,38 @@
//TODO made by AI
import { h } from 'preact';
import { useState } from 'preact/hooks';
import styles from './checkbox.module.css';
function Checkbox({ id, name, value, checked: initialChecked, onChange, label }) {
const [isChecked, setIsChecked] = useState(initialChecked || false);
const handleChange = (event) => {
setIsChecked(event.target.checked);
if (onChange) {
onChange(event); // Propagate the change event
}
};
return (
<div className={styles.checkboxContainer}>
<input
type="checkbox"
id={id}
name={name}
value={value}
checked={isChecked}
onChange={handleChange}
className={styles.nativeCheckbox} // Hide this element
/>
<label htmlFor={id} className={styles.checkbox}>
{/* This div will be our custom visual checkbox */}
<div className={styles.checkmark}>
{isChecked && <span>&#10003;</span>} {/* Checkmark icon */}
</div>
{label && <span className={styles.label}>{label}</span>}
</label>
</div>
);
}
export default Checkbox;

View file

@ -0,0 +1,51 @@
/*TODO made by AI */
.checkboxContainer {
display: flex;
align-items: center;
cursor: pointer;
user-select: none; /* Prevent text selection on click */
}
.nativeCheckbox {
position: absolute;
opacity: 0;
cursor: pointer;
height: 0;
width: 0;
}
.customCheckbox {
position: relative;
display: inline-block;
width: 20px; /* Adjust size as needed */
height: 20px;
background-color: #eee;
border: 1px solid #ccc;
border-radius: 3px; /* Optional rounded corners */
}
.customCheckbox:hover input ~ .checkmark {
background-color: #ddd;
}
.nativeCheckbox:checked ~ .customCheckbox {
background-color: #2196F3; /* Active color */
border: 1px solid #2196F3;
}
.checkmark {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
color: white; /* Checkmark color */
font-size: 16px; /* Adjust checkmark size */
}
.label {
margin-left: 8px;
}

View file

@ -9,18 +9,19 @@ import Thumbnail from '../../assets/mod.svg'
import DownloadIcon from '../../assets/download_alt.svg' import DownloadIcon from '../../assets/download_alt.svg'
function GridCard({item}) { function GridCard({item}) {
const item_page = "/mods/" + item.name;
return ( return (
<div className={styles.rowCard}> <div className={styles.rowCard}>
<a href={'/mods/' + item.name}> <a href={item_page}>
<img src={Thumbnail} className={styles.thumbnail}></img> <img src={Thumbnail} className={styles.thumbnail}></img>
</a> </a>
<p className={styles.description}> {item.description} <p className={styles.description}> {item.description}
</p> </p>
<div className={styles.titleDiv}> <div className={styles.titleDiv}>
<a className={styles.title}> {item.display_name}</a> <a className={styles.title} href={item_page}> {item.display_name}</a>
<a className={styles.author}> By {item.author}</a> <a className={styles.author}> By {item.author}</a>
</div> </div>
<Button href={"/mods/" + item.name} className={styles.downloadButton}> <Button href={item_page} className={styles.downloadButton}>
<img src={DownloadIcon} className={styles.downloadIcon}></img> <img src={DownloadIcon} className={styles.downloadIcon}></img>
Download Download
</Button> </Button>

View file

@ -18,6 +18,7 @@
height: 5rem; height: 5rem;
width: 5rem; width: 5rem;
background-color: #2a2a2a;
border: #3a3a3a .1rem solid; border: #3a3a3a .1rem solid;
border-radius: .5rem; border-radius: .5rem;
} }
@ -33,6 +34,8 @@
font-size: 1.4rem; font-size: 1.4rem;
font-weight: 600; font-weight: 600;
color: #ffffff;
} }
.author { .author {

View file

@ -0,0 +1,36 @@
// TODO made by AI
import { h } from 'preact';
import styles from './input_field.module.css'; // Optional: CSS Modules
function InputField({
id,
name,
label,
value,
onChange,
error,
placeholder,
required,
className,
...rest // To accept other standard input props
}) {
return (
<div className={styles.formGroup}>
{label && <label htmlFor={id} className={styles.label}>{label}</label>}
<input
type="text" // Or any other input type
id={id}
name={name}
className={`${styles.input} ${error ? styles.inputError : ''} ${className}`}
value={value}
onChange={onChange}
placeholder={placeholder}
required={required}
{...rest} // Pass down other props
/>
{error && <div className={styles.errorMessage}>{error}</div>}
</div>
);
}
export default InputField

View file

@ -0,0 +1,43 @@
.formGroup {
margin-bottom: 1rem;
}
.label {
display: block;
margin-bottom: 0.5rem;
}
.input {
width: 100%;
padding: 0.75rem;
font-size: 1rem;
font-family: 'IBM Plex Mono';
color: #eaeaea;
background-color: #2a2a2a;
border: transparent;
border-bottom: .2em solid #3a3a3a;
border-radius: .2rem;
box-sizing: border-box; /* (ensure padding doesn't affect width) */
}
.input:focus {
outline: none;
border-color: #353be5;
background-color: #2a2a2a;
color: #ffffff;
}
.inputError {
border-color: #de3535;
}
.errorMessage {
color: #de3535;
font-size: 0.875rem;
margin-top: 0.25rem;
}

View file

@ -0,0 +1,36 @@
// Functions
import { h } from 'preact';
// Images
import logo from '../assets/logo.png'
import dl_icon from '../assets/download.svg'
// Styles
import '../styles/home.css'
// Components
import Button from '../components/Buttons/button';
function AboutPage() {
return (
<>
<a href='https://radio.oblic-parallels.fr' target="_blank">
<img src={logo} class="logo img" alt="WF" />
<a class="logo text"> radio </a>
</a>
<div class='title'>About us</div>
<div class='background'></div>
<div class='halo'></div>
<div class='mainText'>
WF radio is a platform for all your personnal mods and modpacks.
The difference with already existing big platforms is that radio is self-hosted and open-source.
It's meant for your personnal works that you don't want to publish on the said platforms, but feel free to use it the way you want.
Don't hesitate to learn more about the project with the link below (not here yet).
</div>
</>
);
}
export default AboutPage;

View file

@ -16,12 +16,12 @@ function HomePage() {
return ( return (
<> <>
<div href="https://wf.oblic-parallels.fr" target="_blank"> <a href='https://radio.oblic-parallels.fr' target="_blank">
<img src={logo} class="logo img" alt="WF" /> <img src={logo} class="logo img" alt="WF" />
<a class="logo text"> radio </a> <a class="logo text"> radio </a>
</div> </a>
<div class='title'>An open place for mods</div> <div class='title'>An open place for mods</div>
<Button className='start-button'>Get started</Button> <Button className='start-button' href='/mods'>Get started</Button>
<div class='background'></div> <div class='background'></div>
<div class='halo'></div> <div class='halo'></div>
</> </>

View file

@ -0,0 +1,81 @@
// Preact
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
// Functions
import { createMod } from '../services/api';
// Components
import InputField from '../components/Fields/input_field'
// Images
import logo from '../assets/logo.png'
import profile from '../assets/profile.svg'
// Styles
import styles from '../styles/content_creation.module.css'
import Button from '../components/Buttons/button';
function ModCreationPage() {
const user = "guill" //TODO
const mod_infos = {
name: ""
}
return (
<>
<a href="/">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> creator </p>
</a>
<div className={styles.container}>
<div className={styles.form}>
{/* Name */}
<InputField
id="name"
name="name"
value={mod_infos.name}
// onChange={handleNameChange}
// error={nameError}
placeholder="name"
required
className={styles.smallField}
>
</InputField>
{/* Display name */}
<InputField
id="display_name"
name="display_name"
value={mod_infos.display_name}
// onChange={handleNameChange}
// error={nameError}
placeholder="display_name"
className={styles.smallField}
>
</InputField>
<Button className={styles.createButton}>
Create
</Button>
</div>
<div className={styles.infosPanel}>
<a href={"/users/" + user}>
<img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{user}</p>
</a>
</div>
</div>
</>)
}
export default ModCreationPage

View file

@ -0,0 +1,120 @@
// Preact
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
// Functions
import { fetchMod } from '../services/api';
// Components
// Images
import logo from '../assets/logo.png'
import profile from '../assets/profile.svg'
import download_icon from '../assets/download_alt.svg'
import favorite_icon from '../assets/favorite.svg'
// Styles
import styles from '../styles/content_page.module.css'
function ModPage({name}) {
// UseState
const [mod, setMod] = useState({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
// UseEffect
useEffect(() => {
async function loadItems() {
setLoading(true);
setError(false);
try {
const fetched_mod = await fetchMod(name);
setMod(fetched_mod);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadItems();
}, []); // <-- Tells useEffect to run once after render
if (loading) {
// TODO replace by loading screen
return <div>Loading mod</div>
}
if (error) {
// TODO replace by popup
return <div>Couldn't load mod: {error}</div>
}
return (
<>
<a href="/">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
<div className={styles.container}>
<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>
<div className={styles.infosPanel}>
<a href={"/users/" + mod.author}>
<img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{mod.author}</p>
</a>
<div className={styles.countsContainer}>
<div className={styles.count}>
<img src={download_icon} className={styles.countIcon}></img>
{mod.mod_infos.downloads_count || 0}
</div>
<div className={styles.count}>
<img src={favorite_icon} className={styles.countIcon}></img>
{mod.mod_infos.favorites_count || 0}
</div>
</div>
<p className={styles.panelTitle}>Versions</p>
<div className={styles.timeline}>
<a href={"/notfound"}>
<svg className={styles.versionDot}>
<circle cx="11" cy="11" r="10" stroke="#3a3a3a" stroke-width="1" fill="#1a1a1a" />
</svg>
<div className={styles.version}>
v3.0
</div>
</a>
<a href={"/notfound"}>
<svg className={styles.versionDot}>
<circle cx="11" cy="11" r="10" stroke="#3a3a3a" stroke-width="1" fill="#1a1a1a" />
</svg>
<div className={styles.version}>
v2.0
</div>
</a>
<a href={"/notfound"}>
<svg className={styles.versionDot}>
<circle cx="11" cy="11" r="10" stroke="#3a3a3a" stroke-width="1" fill="#1a1a1a" />
</svg>
<div className={styles.version}>
v1.0
</div>
</a>
</div>
</div>
</div>
</>)
}
export default ModPage

View file

@ -0,0 +1,30 @@
// Functions
import { h } from 'preact';
// Images
import logo from '../assets/logo.png'
import dl_icon from '../assets/download.svg'
// Styles
import '../styles/home.css'
// Components
import Button from '../components/Buttons/button';
function ModpacksPage() {
return (
<>
<a href='https://radio.oblic-parallels.fr' target="_blank">
<img src={logo} class="logoSmall img" alt="WF" />
<a class="logoSmall text"> modpacks </a>
</a>
<div class='title'>Coming soon</div>
<div class='background'></div>
<div class='halo'></div>
</>
);
}
export default ModpacksPage;

View file

@ -12,7 +12,6 @@ import FiltersPanel from '../components/Filters/panel'
import logo from '../assets/logo.png' import logo from '../assets/logo.png'
// Styles // Styles
import '../styles/mods.css'
import GridCard from '../components/Cards/grid'; import GridCard from '../components/Cards/grid';
import RowCard from '../components/Cards/row'; import RowCard from '../components/Cards/row';
@ -51,16 +50,12 @@ function ModsPage() {
return <div>Couldn't load mods: {error}</div> return <div>Couldn't load mods: {error}</div>
} }
// Debugging
console.debug(mods);
const testArray = [(1, 'a'), (2, 'b'), (3, 'c')];
return ( return (
<> <>
<div href="https://wf.oblic-parallels.fr" target="_blank"> <a href="https://radio.oblic-parallels.fr" target="_blank">
<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"> mods </p>
</div> </a>
<FiltersPanel></FiltersPanel> <FiltersPanel></FiltersPanel>
<div class='content-container'> <div class='content-container'>
{/* <GridCard>Test card</GridCard> */} {/* <GridCard>Test card</GridCard> */}
@ -71,11 +66,6 @@ function ModsPage() {
// return <div key={mod.name}>Test</div> // return <div key={mod.name}>Test</div>
return <RowCard key={mod.name} item={mod}/> return <RowCard key={mod.name} item={mod}/>
})} })}
{testArray.map((item, index) => {
// <GridCard key={key}><GridCard/>
console.debug(index, item)
})}
</div> </div>
</> </>
); );

View file

@ -13,3 +13,51 @@ export async function fetchMods() {
return []; 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',
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);
return null;
}
}
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

@ -22,6 +22,10 @@ body {
margin: 0; margin: 0;
} }
* {
transition: 200ms;
}
a { a {
text-decoration: none; text-decoration: none;
@ -56,6 +60,33 @@ a {
user-select: none; user-select: none;
} }
.logoSmall.img {
position: absolute;
top: 1.5rem;
left: .5rem;
height: 6em;
padding: 1.5em;
user-select: none;
}
.logoSmall.text {
position: absolute;
left: 8rem;
top: 4.1rem;
margin: 0;
color: #eaeaea;
font-family: Inter;
font-weight: 600;
font-size: 2.5em;
user-select: none;
}
h1 { h1 {
position: absolute; position: absolute;
left: 0rem; left: 0rem;
@ -94,12 +125,12 @@ h1 {
position: absolute; position: absolute;
top: 11rem; top: 11rem;
right: 4rem; right: 4rem;
left: 4rem; left: 22rem;
bottom: 4rem; bottom: 4rem;
padding: 3rem; padding: 3rem;
background-color: #101010; background-color: #1a1a1a;
color: #eaeaea; color: #eaeaea;
border: #3a3a3a solid; border: #3a3a3a solid;
@ -107,3 +138,30 @@ h1 {
border-radius: .5rem; border-radius: .5rem;
} }
/* Page transitions */
.page {
position: absolute;
top: 0;
left: 0;
width: 100%;
opacity: 0;
transition: opacity 0.2s ease-out; /* Adjust duration and easing */
}
.page-enter {
opacity: 0;
}
.page-enter-active {
opacity: 1;
}
.page-exit {
opacity: 1;
}
.page-exit-active {
opacity: 0;
}

View file

@ -0,0 +1,93 @@
.container {
position: absolute;
top: 11rem;
right: 4rem;
left: 22rem;
bottom: 4rem;
display: inline;
--gap: 1rem;
}
.form {
position: absolute;
top: 0;
bottom: 0;
width: calc(70% - var(--gap));
min-height: 20rem;
padding: 3rem;
background-color: #1a1a1a;
color: #eaeaea;
border: #3a3a3a solid;
border-width: .1em;
border-radius: .5rem;
overflow: scroll;
}
.infosPanel {
position: absolute;
top: 0;
bottom: 0;
left: calc(70% + var(--gap) + 7rem);
right: 0;
padding: 2rem;
min-height: 20rem;
align-items: center;
background-color: #1a1a1a;
color: #eaeaea;
border: #3a3a3a solid;
border-width: .1em;
border-radius: .5rem;
display: inline;
overflow: scroll;
}
.profilePicture {
/*TODO keep it center when overlapping*/
margin-top: 1em;
margin-bottom: 1em;
display: block;
margin-left: auto;
margin-right: auto;
width: 7em;
}
.author {
margin: 0;
margin-bottom: 1rem;
color: #c5c5c5;
font-weight: 600;
font-size: 2rem;
text-align: center;
}
.panelTitle {
margin-top: 2rem;
color: #eaeaea;
font-weight: 600;
font-size: 1.2em;
}
.smallField {
width: 20em;
}
.createButton {
position: absolute;
bottom: 1em;
right: 1em;
font-size: 1.4rem;
}

View file

@ -0,0 +1,197 @@
.container {
position: absolute;
top: 11rem;
right: 4rem;
left: 22rem;
bottom: 4rem;
display: inline;
--gap: 1rem;
}
.title {
margin: 0;
margin-bottom: 1rem;
color: #ffffff;
font-weight: 600;
font-size: 3rem;
z-index: 2;
}
.fullDescription {
margin: 1rem;
margin-top: 3rem;
color: #dadada;
font-size: 1.2rem;
}
.content {
position: absolute;
top: 0;
bottom: 0;
width: calc(70% - var(--gap));
min-height: 20rem;
padding: 3rem;
background-color: #1a1a1a;
color: #eaeaea;
border: #3a3a3a solid;
border-width: .1em;
border-radius: .5rem;
overflow: scroll;
}
.infosPanel {
position: absolute;
top: 0;
bottom: 0;
left: calc(70% + var(--gap) + 7rem);
right: 0;
padding: 2rem;
min-height: 20rem;
align-items: center;
background-color: #1a1a1a;
color: #eaeaea;
border: #3a3a3a solid;
border-width: .1em;
border-radius: .5rem;
display: inline;
overflow: scroll;
}
.backgroundImage {
position: absolute;
top: 0;
right: 0%;
height: 30%;
width: 70%;
background: url("https://resources.oblic-parallels.fr/example.jpg");
background-size: cover;
border-top-right-radius: .4rem;
-webkit-mask-image: radial-gradient(ellipse at top right, black 0%, #00000010 50%, transparent 70%);
z-index: 0;
}
.profilePicture {
/*TODO keep it center when overlapping*/
margin-top: 1em;
margin-bottom: 1em;
display: block;
margin-left: auto;
margin-right: auto;
width: 7em;
}
.author {
margin: 0;
margin-bottom: 1rem;
color: #c5c5c5;
font-weight: 600;
font-size: 2rem;
text-align: center;
}
.panelTitle {
margin-top: 2rem;
color: #eaeaea;
font-weight: 600;
font-size: 1.2em;
}
.countsContainer {
margin-top: 2rem;
display: flex;
justify-content: center;
}
.count {
padding: .5em;
margin: 0 .5rem;
min-width: 3em;
color: #cacaca;
background-color: #2a2a2a;
border: #3a3a3a .1rem solid;
border-radius: .5rem;
}
.countIcon {
margin-bottom: -.4em;
margin-right: .5em;
}
.timeline {
position: relative;
}
.timeline::after {
position: absolute;
top: 0;
bottom: 0;
left: .2em;
margin-left: -.2rem;
margin-top: 1em;
margin-bottom: 1em;
width: .1rem;
background-color: #3a3a3a;
content: '';
}
.version {
margin-bottom: 2em;
margin-left: 1em;
padding: .2em;
color: #8a8a8a;
font-size: 1.3em;
border-radius: .5em;
transition: 200ms;
}
.versionDot {
position: absolute;
margin-left: -.66em;
margin-top: .7em;
height: 1.5em;
width: 1.5em;
z-index: 2;
}
.versionDot > circle {
transition: 200ms;
}
.versionDot > circle:hover {
fill: #eaeaea;
}
.version:hover {
background-color: #3a3a3a;
}

View file

@ -43,3 +43,15 @@
font-weight: 600; font-weight: 600;
font-size: 2rem; font-size: 2rem;
} }
.mainText {
position: absolute;
top: 48rem;
left: 50%;
transform: translate(-50%, -50%); /*Center (compensate size)*/
color: #eaeaea;
font-size: 1.5em;
text-align: justify;
}