Mod versions and version creation pages

This commit is contained in:
Gu://em_ 2025-05-18 18:50:57 +02:00
parent 45900e9033
commit f8bf646565
10 changed files with 778 additions and 38 deletions

View file

@ -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() {
<ModPage path="/mods/:name"></ModPage>
<ModCreationPage path="/create/mod" ></ModCreationPage>
<ModVersionsPage path="/mods/:name/versions"></ModVersionsPage>
<ModVersionCreationPage path="/mods/:name/versions/create"></ModVersionCreationPage>
<LoginPage path='/login'></LoginPage>
<RegisterPage path='/register'></RegisterPage>

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="M189-180q-53.08 0-89.81-36.85Q62.46-253.69 62-307.32q0-7.91.62-15.37.61-7.46 2.61-15.69l84-336q12.26-47.14 49.93-76.38Q236.82-780 285-780h390q48.18 0 85.84 29.24 37.67 29.24 49.93 76.38l84 336q2 8.23 3.11 16.19 1.12 7.96 1.12 15.87 0 53.63-37.35 89.97Q824.31-180 771.05-180q-36.67 0-67.28-19.31-30.62-19.31-47.08-51.92L627.92-310q-7.3-15-21.92-22.5-14.62-7.5-31-7.5H385q-16.19 0-30.9 7.31-14.72 7.31-22.02 22.69l-28.77 58.77q-15.69 33.38-46.84 52.31Q225.33-180 189-180Zm3.21-40q24.17 0 44.67-13.08 20.5-13.07 30.81-34.69l28-57.77q12.63-25.58 37.05-40.02Q357.15-380 385-380h190q28.31 0 52.46 15.31 24.16 15.31 37.85 39.92l28 57q10.31 21.62 30.81 34.69Q744.62-220 769.33-220q36.41 0 62.43-24.65Q857.77-269.31 858-305q0-2.08-2.77-23.62l-84-335q-8.54-33.15-35.31-54.76Q709.15-740 675-740H285q-34.98 0-61.83 21.62-26.86 21.61-34.4 54.76l-84 335q-1.23 4.47-2.77 22.62 0 36.48 26.65 61.24Q155.31-220 192.21-220ZM540-529.23q12.38 0 21.58-9.19 9.19-9.2 9.19-21.58 0-12.38-9.19-21.58-9.2-9.19-21.58-9.19-12.38 0-21.58 9.19-9.19 9.2-9.19 21.58 0 12.38 9.19 21.58 9.2 9.19 21.58 9.19Zm80-80q12.38 0 21.58-9.19 9.19-9.2 9.19-21.58 0-12.38-9.19-21.58-9.2-9.19-21.58-9.19-12.38 0-21.58 9.19-9.19 9.2-9.19 21.58 0 12.38 9.19 21.58 9.2 9.19 21.58 9.19Zm0 160q12.38 0 21.58-9.19 9.19-9.2 9.19-21.58 0-12.38-9.19-21.58-9.2-9.19-21.58-9.19-12.38 0-21.58 9.19-9.19 9.2-9.19 21.58 0 12.38 9.19 21.58 9.2 9.19 21.58 9.19Zm80-80q12.38 0 21.58-9.19 9.19-9.2 9.19-21.58 0-12.38-9.19-21.58-9.2-9.19-21.58-9.19-12.38 0-21.58 9.19-9.19 9.2-9.19 21.58 0 12.38 9.19 21.58 9.2 9.19 21.58 9.19Zm-360.04 56.92q7.66 0 12.69-5.01 5.04-5.01 5.04-12.68v-52.31H410q7.67 0 12.68-5t5.01-12.65q0-7.66-5.01-12.69-5.01-5.04-12.68-5.04h-52.31V-630q0-7.67-5-12.68t-12.65-5.01q-7.66 0-12.69 5.01-5.04 5.01-5.04 12.68v52.31H270q-7.67 0-12.68 5t-5.01 12.65q0 7.66 5.01 12.69 5.01 5.04 12.68 5.04h52.31V-490q0 7.67 5 12.68t12.65 5.01ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 2 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="M360-160v-40h80v-80H184.62q-27.62 0-46.12-18.5Q120-317 120-344.62v-390.76q0-27.62 18.5-46.12Q157-800 184.62-800h590.76q27.62 0 46.12 18.5Q840-763 840-735.38v390.76q0 27.62-18.5 46.12Q803-280 775.38-280H520v80h80v40H360ZM184.62-320h590.76q9.24 0 16.93-7.69 7.69-7.69 7.69-16.93v-390.76q0-9.24-7.69-16.93-7.69-7.69-16.93-7.69H184.62q-9.24 0-16.93 7.69-7.69 7.69-7.69 16.93v390.76q0 9.24 7.69 16.93 7.69 7.69 16.93 7.69ZM160-320v-440 440Z"/></svg>

After

Width:  |  Height:  |  Size: 560 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="M358.15-160H200q-16.85 0-28.42-11.58Q160-183.15 160-200v-158.15q34.15-10 57.08-37.81Q240-423.77 240-460q0-36.23-22.92-64.04-22.93-27.81-57.08-37.81V-720q0-16.85 11.58-28.42Q183.15-760 200-760h160q10.77-34.31 37.85-54.85 27.07-20.54 62.15-20.54t62.15 20.54Q549.23-794.31 560-760h160q16.85 0 28.42 11.58Q760-736.85 760-720v160q34.31 10.77 54.85 37.85 20.54 27.07 20.54 62.15t-20.54 62.15Q794.31-370.77 760-360v160q0 16.85-11.58 28.42Q736.85-160 720-160H561.85q-10.77-36.15-38.81-58.08Q495-240 460-240t-63.04 21.92q-28.04 21.93-38.81 58.08ZM200-200h131.92q17.08-39.85 52.77-59.92Q420.38-280 460-280q39.62 0 75.31 20.08Q571-239.85 588.08-200H720v-195.38h18.46q28.77-4.62 42.85-23.7 14.07-19.07 14.07-40.92t-14.07-40.92q-14.08-19.08-42.85-23.7H720V-720H524.62v-18.46q-4.62-28.77-23.7-42.85-19.07-14.07-40.92-14.07t-40.92 14.07q-19.08 14.08-23.7 42.85V-720H200v131.08q37.08 17.69 58.54 52.77Q280-501.08 280-460q0 40.85-21.46 75.92-21.46 35.08-58.54 53V-200Zm260-260Z"/></svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -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 (
<>
<a href="/">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> creator </p>
</a>
<div className={styles.container}>
<form className={styles.form} onSubmit={handleSubmit}>
<div className={styles.topFields}>
{/* Version number */}
<InputField
id="version_number"
name="version_number"
value={version_number}
onChange={handleVersionNumberChange}
error={field_errors.version_number}
placeholder="version_number"
required
className={styles.smallField}
>
</InputField>
{/* Channel */}
<InputField
id="channel"
name="channel"
value={channel}
onChange={handleChannelChange}
error={field_errors.channel}
placeholder="channel"
className={styles.smallField}
>
</InputField>
</div>
<div className={styles.middleFields}>
{/* Game version */}
<InputField
id="game_version"
name="game_version"
value={game_version}
onChange={handleGameVersionChange}
error={field_errors.game_version}
placeholder="game_version"
className={styles.smallField}
>
</InputField>
{/* Platform */}
<InputField
id="platform"
name="platform"
value={platform}
onChange={handlePlatformChange}
error={field_errors.platform}
placeholder="platform"
className={styles.smallField}
>
</InputField>
{/* Environment */}
<InputField
id="environment"
name="environment"
value={environment}
onChange={handleEnvironmentChange}
error={field_errors.environment}
placeholder="environment"
className={styles.smallField}
>
</InputField>
</div>
<div className={styles.bottomFields}>
{/* Full description */}
<TextArea
id="changelog"
name="changelog"
value={changelog}
onChange={handleChangelogChange}
error={field_errors.changelog}
placeholder="changelog"
containerClass={styles.changelogField}
>
</TextArea>
</div>
<div className={styles.bottomBottomFields}>
{/* Version url */}
<InputField
id="version_url"
name="version_url"
value={version_url}
onChange={handleVersionUrlChange}
error={field_errors.version_url}
placeholder="version_url"
className={styles.smallField}
>
</InputField>
</div>
<Button className={styles.createButton}>
Publish
</Button>
</form>
<div className={styles.infosPanel}>
<img src={profile} className={styles.profilePicture}></img>
<p className={styles.author}>{user}</p>
</div>
</div>
</>)
}
export default ModVersionCreationPage

View file

@ -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}) {
<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 (latest)
</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>
{versions.map( (version) => {
const url = `/mods/${name}/versions`;
return (
<a href={url}>
<svg className={styles.versionDot}>
<circle
cx="11" cy="11" r="10"
stroke="#3a3a3a" stroke-width="1"
fill="#1a1a1a"
href={url}
/>
</svg>
<div className={styles.version} href={url}>
{`${version.channel} ${version.version_number}`}
</div>
</a>)
})}
</div>
{ owner ? (
<div className={styles.panelActions}>
<Button
// variant='secondary'
className={styles.deleteButton}
href={`/mods/${name}/versions/create`}
>
Add version
</Button>
<Button
variant='delete'
className={styles.deleteButton}

View file

@ -0,0 +1,220 @@
// Preact
import { h } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode'
// Functions
import { getModVersions, getMod } from '../services/mods';
// Components
import Button from '../components/Buttons/button';
// Images
import logo from '../assets/logo.png'
import download_icon from '../assets/download_alt.svg'
import GameIcon from '../assets/game.svg'
import LoaderIcon from '../assets/loader.svg'
import PlatformIcon from '../assets/hardware_platform.svg'
// Styles
import styles from '../styles/content_page.module.css'
function ModVersionsPage({name}) {
// UseState
const [mod, setMod] = useState({})
const [versions, setVersions] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState(null)
const [owner, setOwner] = useState(false);
const [currentVersion, setCurrentVersion] = useState(null);
// UseEffect
useEffect(() => {
// Load mod informations
async function loadItems() {
setLoading(true);
setError(false);
try {
const fetched_mod = await getMod(name);
setMod(fetched_mod);
const fetched_versions = await getModVersions(name, {});
setVersions(fetched_versions);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
}
loadItems();
}, []); // <-- Tells useEffect to run once after render
// Handles
const handleDeleteVersion = async () => {
//TODO
console.warn("Cannot delete version: Not implemented");
// await deleteMod(mod.name);
// location.replace('/dashboard');
};
const handleSelectVersion = (version) => {
console.debug("Selected ", version);
setCurrentVersion(version)
}
if (currentVersion == null) {
//TODO make it work
if (versions.length != 0) {
setCurrentVersion(versions[0]);
console.debug(currentVersion);
}
}
// 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 = (
<>
<a href="/">
<img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p>
</a>
</>
);
if (loading) {
// TODO replace by loading screen
return (
<>
{base_page}
<div className={styles.container}>
<div className={styles.content}>
<p className={styles.title}>Loading...</p>
</div>
<div className={styles.infosPanel}> </div>
</div>
</>
);
}
if (error) {
// TODO replace by popup
return (
<>
{base_page}
<div className={styles.container}>
<div className={styles.content}>
<p className={styles.title}>Couldn't load mod versions</p>
<p className={styles.fullDescription}>{error}</p>
</div>
<div className={styles.infosPanel}> </div>
</div>
</>
);
}
return (
<>
{base_page}
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.backgroundImage}></div>
<p className={styles.title}>Versions</p>
<div className={styles.timeline}>
{versions.map( (version) => {
return (
<a href={versions.url}>
<svg className={styles.versionDot}>
<circle
cx="11" cy="11" r="10"
stroke="#3a3a3a" stroke-width="1"
fill="#1a1a1a"
onClick={() => {handleSelectVersion(version)}}/>
</svg>
<div className={styles.version} onClick={() => {handleSelectVersion(version)}}>
{`${version.channel} ${version.version_number}`}
</div>
</a>)
})}
</div>
</div>
<div className={styles.infosPanel}>
{currentVersion ? (
<>
<div className={styles.bigPanelTitle}>
{`${currentVersion.version_number} ${currentVersion.channel}`}
</div>
<div className={styles.compatContainer}>
<div className={styles.count}>
<img src={GameIcon} className={styles.countIcon} alt='Game version'></img>
{ currentVersion.game_version || '?'}
</div>
<div className={styles.count}>
<img src={LoaderIcon} className={styles.countIcon} alt='Mod loader'></img>
{ currentVersion.platform || '?'}
</div>
<div className={styles.count}>
<img src={PlatformIcon} className={styles.countIcon} alt='Environment'></img>
{ currentVersion.environment || '?'}
</div>
</div>
<div className={styles.countsContainer}>
<div className={styles.count}>
<img src={download_icon} className={styles.countIcon} alt='Downloads count'></img>
{mod.mod_infos.downloads_count || 0}
</div>
<Button className={styles.countButton} href={currentVersion.url} >Download</Button>
</div>
<div className={styles.fullChangelog}>
{currentVersion.changelog || "No changelog"}
</div>
{ owner ? (
<div className={styles.panelActions}>
<Button
variant='delete'
className={styles.deleteButton}
onClick={handleDeleteVersion}
>
Delete
</Button>
</div>
) : (
<> </>
)
}
</>
) : (
<>
</>
)}
</div>
</div>
</>)
}
export default ModVersionsPage

View file

@ -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;
}
}

View file

@ -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;
}

View file

@ -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;
}