Mod, createMod and About pages, input fields, some more content and a LOT of fixes (including backend)
This commit is contained in:
parent
a7cf958770
commit
534d251852
|
@ -43,7 +43,7 @@ async function getModByName(req, res) {
|
|||
try {
|
||||
// Query
|
||||
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);
|
||||
} catch (error) {
|
||||
handleError(error, res);
|
||||
|
|
|
@ -14,24 +14,25 @@ async function getModByName(name) {
|
|||
|
||||
}
|
||||
|
||||
async function getModFullInfos(name) {
|
||||
async function getFullModInfos(name) {
|
||||
|
||||
// Query
|
||||
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
|
||||
FROM ModInfos WHERE name = ?`, [name]);
|
||||
const tags = getModTags(name);
|
||||
FROM ModInfos WHERE mod = ?`, [name]);
|
||||
const tags = listTags(name);
|
||||
|
||||
// Merge
|
||||
const res = {...await base_infos, ...await other_infos, ...tags};
|
||||
|
||||
return res;
|
||||
return [await base_infos, await other_infos, await tags];
|
||||
}
|
||||
|
||||
async function listVersions(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) {
|
||||
return await db.query(`SELECT * FROM ModVersions
|
||||
WHERE mod = ?
|
||||
|
@ -184,8 +185,8 @@ async function containsTag(name, tag) {
|
|||
|
||||
// --- Exports ---
|
||||
|
||||
module.exports = { getAllMods, getModByName, getModFullInfos,
|
||||
listVersions, getVersionByNumber, getVersion,
|
||||
module.exports = { getAllMods, getModByName, getFullModInfos,
|
||||
listVersions, listTags, getVersionByNumber, getVersion,
|
||||
createMod, addVersion, addTags,
|
||||
updateMod,
|
||||
deleteMod, deleteVersion, deleteTags,
|
||||
|
|
|
@ -12,7 +12,7 @@ async function getAllMods() {
|
|||
}
|
||||
|
||||
async function getModByName(name) {
|
||||
const res = model.getModByName(name);
|
||||
const res = await model.getModByName(name);
|
||||
if (res.length == 0) {
|
||||
throw new AppError(404, "Cannot find mod with this name", "Not found");
|
||||
}
|
||||
|
@ -20,16 +20,20 @@ async function getModByName(name) {
|
|||
}
|
||||
|
||||
async function getFullModInfos(name) {
|
||||
const res = model.getFullModInfos(name);
|
||||
if (res.length == 0) {
|
||||
throw new AppError(404, "Cannot find mod with this name", "Not found");
|
||||
const [base_infos, other_infos, tags] = await model.getFullModInfos(name);
|
||||
// Check
|
||||
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) {
|
||||
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) {
|
||||
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
|
||||
await sanitizeModData(mod_data); // Sanitize
|
||||
//TODO
|
||||
mod_infos.creation_date = 0
|
||||
mod_infos.creation_date = Date.now();
|
||||
|
||||
// Write changes to database
|
||||
await model.createMod(name, display_name, author, description, mod_infos);
|
||||
|
|
|
@ -7,8 +7,11 @@ import { Router } from 'preact-router';
|
|||
// Pages
|
||||
import HomePage from './pages/home';
|
||||
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 ModPage from './pages/mod_page'
|
||||
import ModCreationPage from './pages/mod_creation'
|
||||
|
||||
// Components
|
||||
import NavBar from './components/NavBar/navbar'
|
||||
|
@ -30,8 +33,12 @@ export function App() {
|
|||
<Router>
|
||||
<HomePage path="/" />
|
||||
<ModsPage path="/mods" />
|
||||
{/* <AboutPage path="/about" /> */}
|
||||
<ModpacksPage path="/modpacks" />
|
||||
<AboutPage path="/about" />
|
||||
<SettingsPage path="/settings" />
|
||||
|
||||
<ModPage path="/mods/:name"></ModPage>
|
||||
<ModCreationPage path="/create/mod" ></ModCreationPage>
|
||||
</Router>
|
||||
|
||||
|
||||
|
|
BIN
frontend/src/assets/example.jpg
Normal file
BIN
frontend/src/assets/example.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 439 KiB |
1
frontend/src/assets/favorite.svg
Normal file
1
frontend/src/assets/favorite.svg
Normal 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 |
1
frontend/src/assets/profile.svg
Normal file
1
frontend/src/assets/profile.svg
Normal 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 |
|
@ -1,18 +1,20 @@
|
|||
import { h } from 'preact' // Necessary ?
|
||||
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 || ''}`
|
||||
|
||||
return (
|
||||
<button
|
||||
className={buttonClasses}
|
||||
onClick= {onClick}
|
||||
{...rest} // Allow passing other props like 'disabled', 'type', etc.
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
<a href={href}>
|
||||
<button
|
||||
className={buttonClasses}
|
||||
onClick= {onClick}
|
||||
{...rest} // Allow passing other props like 'disabled', 'type', etc.
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
</a>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
transition: 300ms;
|
||||
}
|
||||
|
||||
.button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.primary {
|
||||
background-color: #353be5;
|
||||
border-color: #4e53dc;
|
||||
|
|
|
@ -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>✓</span>} {/* Checkmark icon */}
|
||||
</div>
|
||||
{label && <span className={styles.label}>{label}</span>}
|
||||
</label>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default Checkbox;
|
|
@ -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;
|
||||
}
|
|
@ -9,18 +9,19 @@ import Thumbnail from '../../assets/mod.svg'
|
|||
import DownloadIcon from '../../assets/download_alt.svg'
|
||||
|
||||
function GridCard({item}) {
|
||||
const item_page = "/mods/" + item.name;
|
||||
return (
|
||||
<div className={styles.rowCard}>
|
||||
<a href={'/mods/' + item.name}>
|
||||
<img src={Thumbnail} className={styles.thumbnail}></img>
|
||||
</a>
|
||||
<a href={item_page}>
|
||||
<img src={Thumbnail} className={styles.thumbnail}></img>
|
||||
</a>
|
||||
<p className={styles.description}> {item.description}
|
||||
</p>
|
||||
<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>
|
||||
</div>
|
||||
<Button href={"/mods/" + item.name} className={styles.downloadButton}>
|
||||
<Button href={item_page} className={styles.downloadButton}>
|
||||
<img src={DownloadIcon} className={styles.downloadIcon}></img>
|
||||
Download
|
||||
</Button>
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
height: 5rem;
|
||||
width: 5rem;
|
||||
|
||||
background-color: #2a2a2a;
|
||||
border: #3a3a3a .1rem solid;
|
||||
border-radius: .5rem;
|
||||
}
|
||||
|
@ -33,6 +34,8 @@
|
|||
|
||||
font-size: 1.4rem;
|
||||
font-weight: 600;
|
||||
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.author {
|
||||
|
|
36
frontend/src/components/Fields/input_field.jsx
Normal file
36
frontend/src/components/Fields/input_field.jsx
Normal 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
|
43
frontend/src/components/Fields/input_field.module.css
Normal file
43
frontend/src/components/Fields/input_field.module.css
Normal 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;
|
||||
}
|
0
frontend/src/components/Fields/search.module.css
Normal file
0
frontend/src/components/Fields/search.module.css
Normal 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;
|
|
@ -16,12 +16,12 @@ function HomePage() {
|
|||
|
||||
return (
|
||||
<>
|
||||
<div href="https://wf.oblic-parallels.fr" target="_blank">
|
||||
<img src={logo} class="logo img" alt="WF" />
|
||||
<a class="logo text"> radio </a>
|
||||
</div>
|
||||
<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'>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='halo'></div>
|
||||
</>
|
||||
|
|
81
frontend/src/pages/mod_creation.jsx
Normal file
81
frontend/src/pages/mod_creation.jsx
Normal 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
|
120
frontend/src/pages/mod_page.jsx
Normal file
120
frontend/src/pages/mod_page.jsx
Normal 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
|
30
frontend/src/pages/modpacks.jsx
Normal file
30
frontend/src/pages/modpacks.jsx
Normal 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;
|
|
@ -12,7 +12,6 @@ import FiltersPanel from '../components/Filters/panel'
|
|||
import logo from '../assets/logo.png'
|
||||
|
||||
// Styles
|
||||
import '../styles/mods.css'
|
||||
import GridCard from '../components/Cards/grid';
|
||||
import RowCard from '../components/Cards/row';
|
||||
|
||||
|
@ -51,16 +50,12 @@ function ModsPage() {
|
|||
return <div>Couldn't load mods: {error}</div>
|
||||
}
|
||||
|
||||
// Debugging
|
||||
console.debug(mods);
|
||||
const testArray = [(1, 'a'), (2, 'b'), (3, 'c')];
|
||||
|
||||
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" />
|
||||
<a class="logo text"> radio </a>
|
||||
</div>
|
||||
<p class="logo text"> mods </p>
|
||||
</a>
|
||||
<FiltersPanel></FiltersPanel>
|
||||
<div class='content-container'>
|
||||
{/* <GridCard>Test card</GridCard> */}
|
||||
|
@ -71,11 +66,6 @@ function ModsPage() {
|
|||
// return <div key={mod.name}>Test</div>
|
||||
return <RowCard key={mod.name} item={mod}/>
|
||||
})}
|
||||
|
||||
{testArray.map((item, index) => {
|
||||
// <GridCard key={key}><GridCard/>
|
||||
console.debug(index, item)
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -12,4 +12,52 @@ export async function fetchMods() {
|
|||
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',
|
||||
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;
|
||||
}
|
||||
}
|
|
@ -22,6 +22,10 @@ body {
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
* {
|
||||
transition: 200ms;
|
||||
}
|
||||
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
|
@ -56,6 +60,33 @@ a {
|
|||
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 {
|
||||
position: absolute;
|
||||
left: 0rem;
|
||||
|
@ -94,16 +125,43 @@ h1 {
|
|||
position: absolute;
|
||||
top: 11rem;
|
||||
right: 4rem;
|
||||
left: 4rem;
|
||||
left: 22rem;
|
||||
bottom: 4rem;
|
||||
|
||||
padding: 3rem;
|
||||
|
||||
background-color: #101010;
|
||||
background-color: #1a1a1a;
|
||||
color: #eaeaea;
|
||||
|
||||
border: #3a3a3a solid;
|
||||
border-width: .1em;
|
||||
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;
|
||||
}
|
93
frontend/src/styles/content_creation.module.css
Normal file
93
frontend/src/styles/content_creation.module.css
Normal 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;
|
||||
}
|
197
frontend/src/styles/content_page.module.css
Normal file
197
frontend/src/styles/content_page.module.css
Normal 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;
|
||||
}
|
|
@ -42,4 +42,16 @@
|
|||
text-align: center;
|
||||
font-weight: 600;
|
||||
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;
|
||||
|
||||
}
|
Loading…
Reference in a new issue