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 {
|
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);
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
|
|
||||||
|
|
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 { 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 (
|
||||||
<button
|
<a href={href}>
|
||||||
className={buttonClasses}
|
<button
|
||||||
onClick= {onClick}
|
className={buttonClasses}
|
||||||
{...rest} // Allow passing other props like 'disabled', 'type', etc.
|
onClick= {onClick}
|
||||||
>
|
{...rest} // Allow passing other props like 'disabled', 'type', etc.
|
||||||
{children}
|
>
|
||||||
</button>
|
{children}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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'
|
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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
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 (
|
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>
|
||||||
</>
|
</>
|
||||||
|
|
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'
|
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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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;
|
||||||
|
}
|
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;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in a new issue