From f8bf646565b58a1ab856196085bdc3ce00fec5ba Mon Sep 17 00:00:00 2001 From: "Gu://em_" Date: Sun, 18 May 2025 18:50:57 +0200 Subject: [PATCH] Mod versions and version creation pages --- frontend/src/app.jsx | 4 + frontend/src/assets/game.svg | 1 + frontend/src/assets/hardware_platform.svg | 1 + frontend/src/assets/loader.svg | 1 + frontend/src/pages/create_mod_version.jsx | 269 ++++++++++++++++++ frontend/src/pages/mod_page.jsx | 81 +++--- frontend/src/pages/mod_versions.jsx | 220 ++++++++++++++ frontend/src/services/mods.js | 62 ++++ frontend/src/styles/content_page.module.css | 39 +++ .../src/styles/version_creation.module.css | 138 +++++++++ 10 files changed, 778 insertions(+), 38 deletions(-) create mode 100644 frontend/src/assets/game.svg create mode 100644 frontend/src/assets/hardware_platform.svg create mode 100644 frontend/src/assets/loader.svg create mode 100644 frontend/src/pages/create_mod_version.jsx create mode 100644 frontend/src/pages/mod_versions.jsx create mode 100644 frontend/src/styles/version_creation.module.css diff --git a/frontend/src/app.jsx b/frontend/src/app.jsx index 0552b25..5e70a9c 100644 --- a/frontend/src/app.jsx +++ b/frontend/src/app.jsx @@ -12,7 +12,9 @@ import ComingSoonPage from './pages/coming_soon'; import ModsPage from './pages/mods'; import ModPage from './pages/mod_page' +import ModVersionsPage from './pages/mod_versions' import ModCreationPage from './pages/mod_creation' +import ModVersionCreationPage from './pages/create_mod_version' import ModpacksPage from './pages/modpacks'; @@ -50,6 +52,8 @@ export function App() { + + diff --git a/frontend/src/assets/game.svg b/frontend/src/assets/game.svg new file mode 100644 index 0000000..8f04fc0 --- /dev/null +++ b/frontend/src/assets/game.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/hardware_platform.svg b/frontend/src/assets/hardware_platform.svg new file mode 100644 index 0000000..847d932 --- /dev/null +++ b/frontend/src/assets/hardware_platform.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/assets/loader.svg b/frontend/src/assets/loader.svg new file mode 100644 index 0000000..1ef60b2 --- /dev/null +++ b/frontend/src/assets/loader.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/pages/create_mod_version.jsx b/frontend/src/pages/create_mod_version.jsx new file mode 100644 index 0000000..c6eae6f --- /dev/null +++ b/frontend/src/pages/create_mod_version.jsx @@ -0,0 +1,269 @@ +// Preact +import { h } from 'preact'; +import { useState, useEffect } from 'preact/hooks'; +import Cookies from 'js-cookie' +import { jwtDecode } from 'jwt-decode' + +// Functions +import { createModVersion } from '../services/mods'; + +// Components +import InputField from '../components/Fields/input_field' +import TextArea from '../components/Fields/text_area' + +// Images +import logo from '../assets/logo.png' +import profile from '../assets/profile.svg' + +// Styles +import styles from '../styles/version_creation.module.css' +import Button from '../components/Buttons/button'; + +function ModVersionCreationPage({name}) { + //TODO add missing fields + + const null_fields = { + version_number: null, + channel: null, + changelog: null, + game_version: null, + platform: null, + environment: null, + url: null + } + + //TODO use a service + let user; + const token = Cookies.get('authToken'); + if (token) { + const decoded_token = jwtDecode(token); + if (decoded_token) { + user = decoded_token.username; + } else { + location.replace('/login') + } + } else { + location.replace('/login') + } + + const [version_number, setVersionNumber] = useState(''); + const [channel, setChannel] = useState(''); + const [game_version, setGameVersion] = useState(''); + const [platform, setPlatform] = useState(''); + const [environment, setEnvironment] = useState(''); + const [changelog, setChangelog] = useState(''); + const [version_url, setVersionUrl] = useState(''); + + const [publish_status, setPublishStatus] = useState(null); + const [field_errors, setFieldErrors] = useState(null_fields); + + + // Handle fields changes + + const handleVersionNumberChange = (event) => { + setVersionNumber(event.target.value); + // Reset error + setFieldErrors(prevErrors => ({ ...prevErrors, version_number: null })); + }; + const handleChannelChange = (event) => { + setChannel(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, channel: null })); + }; + const handleGameVersionChange = (event) => { + setGameVersion(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, game_version: null })); + }; + const handlePlatformChange = (event) => { + setPlatform(event.target.value); + console.debug(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, platform: null })); + }; + const handleEnvironmentChange = (event) => { + setEnvironment(event.target.value); + console.debug(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, environment: null })); + }; + const handleChangelogChange = (event) => { + setChangelog(event.target.value); + console.debug(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, changelog: null })); + }; + const handleVersionUrlChange = (event) => { + setVersionUrl(event.target.value); + console.debug(event.target.value); + setFieldErrors(prevErrors => ({ ...prevErrors, version_url: null })); + }; + + // Submission + + const handleSubmit = async (event) => { + event.preventDefault(); + setFieldErrors(null_fields); + + setPublishStatus('publishing'); + + try { + // Gather data + const version_data = { + version_number: version_number, + channel: channel, + changelog: changelog, + game_version: changelog, + platform: changelog, + environment: changelog, + url: version_url + } + + // Query + const response = await createModVersion(name, version_data); + + // On success + console.debug('Published successfully:', response); + setPublishStatus('success'); + window.location.replace("/mods/" + name + "/versions"); //TODO no page reloading + + } catch (error) { + + console.error('Creation failed:', error); + setPublishStatus('error'); + + //TODO handle different codes differently + setFieldErrors({ + version_number: ' ', + channel: ' ', + changelog: ' ', + game_version: ' ', + platform: ' ', + environment: ' ', + version_url: ' ' + }); + } + finally { + setPublishStatus(null); + } + }; + + return ( + <> + + + + + +
+
+ +
+ {/* Version number */} + + + + {/* Channel */} + + + +
+
+ {/* Game version */} + + + + {/* Platform */} + + + + {/* Environment */} + + + +
+ +
+ {/* Full description */} + +
+ +
+ + {/* Version url */} + + + +
+ + +
+ + +
+ + +

{user}

+ +
+
+ + ) +} + +export default ModVersionCreationPage \ No newline at end of file diff --git a/frontend/src/pages/mod_page.jsx b/frontend/src/pages/mod_page.jsx index 47cc66a..dcdd2c4 100644 --- a/frontend/src/pages/mod_page.jsx +++ b/frontend/src/pages/mod_page.jsx @@ -5,7 +5,7 @@ import Cookies from 'js-cookie' import { jwtDecode } from 'jwt-decode' // Functions -import { getMod, deleteMod } from '../services/mods'; +import { getMod, deleteMod, getModVersions } from '../services/mods'; // Components import Button from '../components/Buttons/button'; @@ -26,6 +26,7 @@ function ModPage({name}) { const [loading, setLoading] = useState(true) const [error, setError] = useState(null) const [owner, setOwner] = useState(false); + const [versions, setVersions] = useState(null); // UseEffect useEffect(() => { @@ -34,8 +35,11 @@ function ModPage({name}) { setLoading(true); setError(false); try { - const fetched_mod = await getMod(name); - setMod(fetched_mod); + const fetched_mod = getMod(name); + const fetched_versions = getModVersions(name, {}); + setMod( await fetched_mod); + setVersions( await fetched_versions); + } catch (err) { setError(err.message); } finally { @@ -53,18 +57,19 @@ function ModPage({name}) { location.replace('/dashboard'); }; - // Load user informations - //TODO use a service - //? for some reason, doesn't work inside useEffect - const token = Cookies.get('authToken'); - if (token) { - const decoded_token = jwtDecode(token); - if (decoded_token) { - if (decoded_token.username === mod.author) { - setOwner(true); - } + + // Load user informations + //TODO use a service + //? for some reason, doesn't work inside useEffect + const token = Cookies.get('authToken'); + if (token) { + const decoded_token = jwtDecode(token); + if (decoded_token) { + if (decoded_token.username === mod.author) { + setOwner(true); } } + } const base_page = ( <> @@ -139,35 +144,35 @@ function ModPage({name}) {

Versions

- - - - -
- v3.0 (latest) -
-
- - - - -
- v2.0 -
-
- - - - - -
- v1.0 -
-
+ + {versions.map( (version) => { + const url = `/mods/${name}/versions`; + return ( + + + + +
+ {`${version.channel} ${version.version_number}`} +
+
) + })}
{ owner ? (
+ +
+ +
+ {currentVersion.changelog || "No changelog"} +
+ + { owner ? ( +
+ +
+ ) : ( + <> + ) + } + + ) : ( + <> + + + )} + + + + ) +} + +export default ModVersionsPage \ No newline at end of file diff --git a/frontend/src/services/mods.js b/frontend/src/services/mods.js index 7bf810b..e2c26f5 100644 --- a/frontend/src/services/mods.js +++ b/frontend/src/services/mods.js @@ -101,4 +101,66 @@ export async function deleteMod(mod_name) { console.error('Failed to delete mod:', error); throw error; } +} + + +export async function getModVersions(mod_name, filters) { + try { + let url_parameters = ""; + + // Parse filters + for (const [key, value] of Object.entries(filters)) { + if (url_parameters === "") { + url_parameters += "?" + } else { + url_parameters += "&" + } + + url_parameters += `${key}=${value}`; + } + + const response = await fetch(`${API_BASE_URL}/mods/${mod_name}/versions${url_parameters}`, { + method: 'GET', + }); + + if (!response.ok) { + console.error(response.body); //TODO integrate body to error + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + + } catch (error) { + console.error('Failed to delete mod:', error); + throw error; + } +} + + +export async function createModVersion(mod_name, version_data) { + try { + const auth_token = Cookies.get('authToken'); + + const response = await fetch(`${API_BASE_URL}/mods/${mod_name}/versions`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': auth_token, + }, + body: JSON.stringify({...version_data}) + }); + + if (!response.ok) { + console.error(response.body); //TODO integrate body to error + throw new Error(`HTTP error! status: ${response.status}`); + } + + const data = await response.json(); + return data; + + } catch (error) { + console.error('Failed to create mod version:', error); + throw error; + } } \ No newline at end of file diff --git a/frontend/src/styles/content_page.module.css b/frontend/src/styles/content_page.module.css index ae05839..3226693 100644 --- a/frontend/src/styles/content_page.module.css +++ b/frontend/src/styles/content_page.module.css @@ -132,6 +132,18 @@ background-color: #2a2a2a; border: #3a3a3a .1rem solid; border-radius: .5rem; + + user-select: none; +} + +.countButton { + + /* padding: .5em; */ + margin: 0 .5rem; + min-width: 3em; + + height: 100%; + font-size: 1rem; } .countIcon { @@ -171,6 +183,8 @@ font-size: 1.3em; border-radius: .5em; + cursor: pointer; + transition: 200ms; } @@ -183,6 +197,8 @@ width: 1.5em; z-index: 2; + + cursor: pointer; } .versionDot > circle { @@ -206,6 +222,10 @@ width: 100%; + display: flex; + flex-direction: column; + gap: 1rem; + box-sizing: border-box; } @@ -215,4 +235,23 @@ width: 100%; min-width: 5em; font-size: 1.2rem; +} + +.bigPanelTitle { + + font-size: 2rem; + margin-bottom: 2.5rem; +} + +.fullChangelog { + + margin-top: 3rem; + color: #8a8a8a +} + +.compatContainer { + display: flex; + flex-direction: column; + gap: 1rem; + margin-bottom: 3érem; } \ No newline at end of file diff --git a/frontend/src/styles/version_creation.module.css b/frontend/src/styles/version_creation.module.css new file mode 100644 index 0000000..b035c2b --- /dev/null +++ b/frontend/src/styles/version_creation.module.css @@ -0,0 +1,138 @@ +.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; +} + +/* .ChangelogField { + + margin-top: -.01rem; + margin-left: 1rem; + + height: 7em; + min-width: 20em; + display: inline-flex; + flex-grow: 1; +} */ + +.changelogField { + min-width: 20em; + height: 100%; +} + +.topFields { + display: flex; + width: 100%; + gap: 1rem; + margin-bottom: 1rem; +} + +.middleFields { + display: flex; + width: 100%; + gap: 1rem; +} + +.bottomFields { + /* display: flex; + justify-content: right; */ + height: 25vh; + min-height: 12em; +} + +.bottomBottomFields { + display: flex; + width: 100%; + gap: 1rem; + + margin: 2rem 0; +} + + +.createButton { + position: absolute; + bottom: 1em; + right: 1em; + + font-size: 1.4rem; +} \ No newline at end of file