Made dashboard, fixed navbar icons and padding, implemented search bar in mods and other various fixes

This commit is contained in:
Gu://em_ 2025-05-17 00:27:49 +02:00
parent 93bf736ca9
commit 60e557826e
29 changed files with 407 additions and 96 deletions

View file

@ -16,6 +16,7 @@ import ModCreationPage from './pages/mod_creation'
import ModpacksPage from './pages/modpacks'; import ModpacksPage from './pages/modpacks';
import DashboardPage from './pages/dashboard';
import LoginPage from './pages/login'; import LoginPage from './pages/login';
import RegisterPage from './pages/register'; import RegisterPage from './pages/register';
import SettingsPage from './pages/settings'; import SettingsPage from './pages/settings';
@ -45,6 +46,8 @@ export function App() {
<AboutPage path="/about" /> <AboutPage path="/about" />
<SettingsPage path="/settings" /> <SettingsPage path="/settings" />
<DashboardPage path="/dashboard"/>
<ModPage path="/mods/:name"></ModPage> <ModPage path="/mods/:name"></ModPage>
<ModCreationPage path="/create/mod" ></ModCreationPage> <ModCreationPage path="/create/mod" ></ModCreationPage>

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M240.92-268.31q51-37.84 111.12-59.77Q412.15-350 480-350t127.96 21.92q60.12 21.93 111.12 59.77 37.3-41 59.11-94.92Q800-417.15 800-480q0-133-93.5-226.5T480-800q-133 0-226.5 93.5T160-480q0 62.85 21.81 116.77 21.81 53.92 59.11 94.92ZM480.01-450q-54.78 0-92.39-37.6Q350-525.21 350-579.99t37.6-92.39Q425.21-710 479.99-710t92.39 37.6Q610-634.79 610-580.01t-37.6 92.39Q534.79-450 480.01-450ZM480-100q-79.15 0-148.5-29.77t-120.65-81.08q-51.31-51.3-81.08-120.65Q100-400.85 100-480t29.77-148.5q29.77-69.35 81.08-120.65 51.3-51.31 120.65-81.08Q400.85-860 480-860t148.5 29.77q69.35 29.77 120.65 81.08 51.31 51.3 81.08 120.65Q860-559.15 860-480t-29.77 148.5q-29.77 69.35-81.08 120.65-51.3 51.31-120.65 81.08Q559.15-100 480-100Zm0-60q54.15 0 104.42-17.42 50.27-17.43 89.27-48.73-39-30.16-88.11-47Q536.46-290 480-290t-105.77 16.65q-49.31 16.66-87.92 47.2 39 31.3 89.27 48.73Q425.85-160 480-160Zm0-350q29.85 0 49.92-20.08Q550-550.15 550-580t-20.08-49.92Q509.85-650 480-650t-49.92 20.08Q410-609.85 410-580t20.08 49.92Q450.15-510 480-510Zm0-70Zm0 355Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M243.95-254.31q54.9-37.97 111.72-59.73 56.82-21.75 124.33-21.75 67.51 0 124.41 21.75 56.9 21.76 111.8 59.73 42.02-45.05 65.98-101.91 23.96-56.87 23.96-123.78 0-136.38-94.88-231.27-94.89-94.88-231.27-94.88t-231.27 94.88Q153.85-616.38 153.85-480q0 66.91 24.03 123.78 24.04 56.86 66.07 101.91Zm235.89-208.77q-50.22 0-84.55-34.47-34.32-34.48-34.32-84.71t34.48-84.55q34.48-34.32 84.71-34.32 50.22 0 84.55 34.48 34.32 34.48 34.32 84.7 0 50.23-34.48 84.55t-84.71 34.32Zm.47 343.08q-75.46 0-141.01-28.04-65.54-28.04-114.38-76.99-48.84-48.94-76.88-114.22Q120-404.53 120-479.92q0-75.39 28.04-140.75t76.99-114.3q48.94-48.95 114.22-76.99Q404.53-840 479.92-840q75.39 0 140.75 28.04t114.3 76.99q48.95 48.94 76.99 114.32Q840-555.27 840-480.31q0 75.46-28.04 141.01-28.04 65.54-76.99 114.38-48.94 48.84-114.32 76.88Q555.27-120 480.31-120Zm-.31-33.85q56.13 0 111.28-19.42 55.16-19.42 98.05-56.78-42.89-33.41-96.07-52.66-53.18-19.24-113.26-19.24-60.08 0-113.64 18.86-53.57 18.86-95.18 53.04 42.38 37.36 97.54 56.78 55.15 19.42 111.28 19.42Zm.07-343.07q36.39 0 60.75-24.43t24.36-60.82q0-36.39-24.43-60.75t-60.82-24.36q-36.39 0-60.75 24.43t-24.36 60.82q0 36.39 24.43 60.75t60.82 24.36ZM480-582.1Zm0 354.2Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M463.08-463.08H240v-33.84h223.08V-720h33.84v223.08H720v33.84H496.92V-240h-33.84v-223.08Z"/></svg>

After

Width:  |  Height:  |  Size: 213 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M536.92-590.77V-800H800v209.23H536.92ZM160-483.08V-800h263.08v316.92H160ZM536.92-160v-316.92H800V-160H536.92ZM160-160v-209.23h263.08V-160H160Zm33.85-356.92h195.38v-249.23H193.85v249.23Zm376.92 323.07h195.38v-249.23H570.77v249.23Zm0-430.77h195.38v-141.53H570.77v141.53ZM193.85-193.85h195.38v-141.53H193.85v141.53Zm195.38-323.07Zm181.54-107.7Zm0 181.54Zm-181.54 107.7Z"/></svg>

After

Width:  |  Height:  |  Size: 491 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M140-820h283.08v283.08H140V-820Zm33.85 33.69v208.41-208.41ZM536.92-820H820v283.08H536.92V-820Zm40.98 33.69v208.41-208.41ZM140-423.08h283.08V-140H140v-283.08Zm33.85 40.16v209.07-209.07Zm487.94-40.16h33.85v124.36H820v33.85H695.64V-140h-33.85v-124.87H536.92v-33.85h124.87v-124.36Zm-91.02-363.07v215.38h215.38v-215.38H570.77Zm-396.92 0v215.38h215.38v-215.38H173.85Zm0 396.92v215.38h215.38v-215.38H173.85Z"/></svg>

After

Width:  |  Height:  |  Size: 525 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48px" viewBox="0 -960 960 960" width="48px" fill="#e3e3e3"><path d="M480.23-140v-45.39h282.08q4.61 0 8.46-3.84 3.84-3.85 3.84-8.46v-564.62q0-4.61-3.84-8.46-3.85-3.84-8.46-3.84H480.23V-820h282.08q23.53 0 40.61 17.08T820-762.31v564.62q0 23.53-17.08 40.61T762.31-140H480.23Zm-35.77-187.69-33-32.23 97.39-97.39H140v-45.38h367.62l-97.39-97.39 32.62-32.61 153.3 153.5-151.69 151.5Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M479.38-160v-33.85h262.16q9.23 0 16.92-7.69 7.69-7.69 7.69-16.92v-523.08q0-9.23-7.69-16.92-7.69-7.69-16.92-7.69H479.38V-800h262.16q24.58 0 41.52 16.94Q800-766.12 800-741.54v523.08q0 24.58-16.94 41.52Q766.12-160 741.54-160H479.38Zm-20.51-186.41-24.69-23.9 92.77-92.77H160v-33.84h366.64l-92.77-92.77 24.18-24.41 134.26 134.51-133.44 133.18Z"/></svg>

Before

Width:  |  Height:  |  Size: 432 B

After

Width:  |  Height:  |  Size: 463 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="M218.46-160q-24.58 0-41.52-16.94Q160-193.88 160-218.46v-523.08q0-24.58 16.94-41.52Q193.88-800 218.46-800h262.16v33.85H218.46q-9.23 0-16.92 7.69-7.69 7.69-7.69 16.92v523.08q0 9.23 7.69 16.92 7.69 7.69 16.92 7.69h262.16V-160H218.46Zm448.1-186.41-24.69-23.9 92.77-92.77H367.69v-33.84h366.64l-92.77-92.77 24.18-24.41L800-479.59 666.56-346.41Z"/></svg>

After

Width:  |  Height:  |  Size: 463 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="M781.69-136.92 530.46-388.16q-30 24.77-69 38.77-39 14-80.69 14-102.55 0-173.58-71.01-71.03-71.01-71.03-173.54 0-102.52 71.01-173.6 71.01-71.07 173.54-71.07 102.52 0 173.6 71.03 71.07 71.03 71.07 173.58 0 42.85-14.38 81.85-14.39 39-38.39 67.84l251.23 251.23-42.15 42.16ZM380.77-395.38q77.31 0 130.96-53.66 53.66-53.65 53.66-130.96t-53.66-130.96q-53.65-53.66-130.96-53.66t-130.96 53.66Q196.15-657.31 196.15-580t53.66 130.96q53.65 53.66 130.96 53.66Z"/></svg>

After

Width:  |  Height:  |  Size: 572 B

View file

@ -1 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="m405.38-120-14.46-115.69q-19.15-5.77-41.42-18.16-22.27-12.38-37.88-26.53L204.92-235l-74.61-130 92.23-69.54q-1.77-10.84-2.92-22.34-1.16-11.5-1.16-22.35 0-10.08 1.16-21.19 1.15-11.12 2.92-25.04L130.31-595l74.61-128.46 105.93 44.61q17.92-14.92 38.77-26.92 20.84-12 40.53-18.54L405.38-840h149.24l14.46 116.46q23 8.08 40.65 18.54 17.65 10.46 36.35 26.15l109-44.61L829.69-595l-95.31 71.85q3.31 12.38 3.7 22.73.38 10.34.38 20.42 0 9.31-.77 19.65-.77 10.35-3.54 25.04L827.92-365l-74.61 130-107.23-46.15q-18.7 15.69-37.62 26.92-18.92 11.23-39.38 17.77L554.62-120H405.38ZM440-160h78.23L533-268.31q30.23-8 54.42-21.96 24.2-13.96 49.27-38.27L736.46-286l39.77-68-87.54-65.77q5-17.08 6.62-31.42 1.61-14.35 1.61-28.81 0-15.23-1.61-28.81-1.62-13.57-6.62-29.88L777.77-606 738-674l-102.08 42.77q-18.15-19.92-47.73-37.35-29.57-17.42-55.96-23.11L520-800h-79.77l-12.46 107.54q-30.23 6.46-55.58 20.81-25.34 14.34-50.42 39.42L222-674l-39.77 68L269-541.23q-5 13.46-7 29.23t-2 32.77q0 15.23 2 30.23t6.23 29.23l-86 65.77L222-286l99-42q23.54 23.77 48.88 38.12 25.35 14.34 57.12 22.34L440-160Zm38.92-220q41.85 0 70.93-29.08 29.07-29.07 29.07-70.92t-29.07-70.92Q520.77-580 478.92-580q-42.07 0-71.04 29.08-28.96 29.07-28.96 70.92t28.96 70.92Q436.85-380 478.92-380ZM480-480Z"/></svg> <svg xmlns="http://www.w3.org/2000/svg" height="40px" viewBox="0 -960 960 960" width="40px" fill="#e3e3e3"><path d="m410.72-120-15.59-114.87q-21.1-6.59-46.12-20.51-25.01-13.93-42.42-29.88l-106.03 47.03-69.43-122.41 93.51-69.69q-1.92-11.67-3.15-24.45t-1.23-24.45q0-10.9 1.23-23.55 1.23-12.66 3.15-26.89l-93.51-70.2 69.43-120.36 105.26 45.74q19.72-16.2 43.23-29.74 23.51-13.54 45.31-20.23L410.72-840h138.56l15.59 115.64q24.44 8.9 45.43 20.82 20.98 11.92 40.8 29.05l108.85-45.74 68.92 120.36-96.59 71.69q3.46 13.36 4.18 25.24.72 11.89.72 22.94 0 10.28-1.1 22.09-1.11 11.81-3.87 26.96l95.56 70.31-69.44 122.41-107.23-47.8q-20.48 17.49-42 30.59-21.51 13.11-44.23 19.8L549.28-120H410.72Zm28.15-33.85h81.16l14.76-110.25q30.54-8 55.71-22.45t51.06-39.07l101.88 43.83 39.92-69.13-89.8-66.9q4.34-18.05 6.29-32.81 1.95-14.75 1.95-29.37 0-15.9-1.88-29.88-1.87-13.99-6.36-30.76l91.34-68.44-39.93-69.13-104.17 43.9q-18.83-20.9-49.04-39.14-30.22-18.24-57.73-22.45l-12.9-110.25h-82.18l-12.46 109.48q-31.72 6.31-57.79 21.14-26.06 14.84-51.29 40.38l-102.38-43.06-39.93 69.13 89.54 66.05q-4.85 14.29-6.92 30.29-2.08 16-2.08 33.51 0 15.9 2.08 31.13 2.07 15.23 6.15 30.28l-88.77 66.9 39.93 69.13 101.61-43.13q23.85 24.43 50.17 39.01 26.32 14.58 58.91 22.58l13.15 109.48Zm39.23-229.23q40.72 0 68.82-28.1 28.11-28.1 28.11-68.82 0-40.72-28.11-68.82-28.1-28.1-68.82-28.1-40.28 0-68.6 28.1-28.32 28.1-28.32 68.82 0 40.72 28.32 68.82 28.32 28.1 68.6 28.1ZM480-480Z"/></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 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="m405.38-120-14.46-115.69q-19.15-5.77-41.42-18.16-22.27-12.38-37.88-26.53L204.92-235l-74.61-130 92.23-69.54q-1.77-10.84-2.92-22.34-1.16-11.5-1.16-22.35 0-10.08 1.16-21.19 1.15-11.12 2.92-25.04L130.31-595l74.61-128.46 105.93 44.61q17.92-14.92 38.77-26.92 20.84-12 40.53-18.54L405.38-840h149.24l14.46 116.46q23 8.08 40.65 18.54 17.65 10.46 36.35 26.15l109-44.61L829.69-595l-95.31 71.85q3.31 12.38 3.7 22.73.38 10.34.38 20.42 0 9.31-.77 19.65-.77 10.35-3.54 25.04L827.92-365l-74.61 130-107.23-46.15q-18.7 15.69-37.62 26.92-18.92 11.23-39.38 17.77L554.62-120H405.38ZM440-160h78.23L533-268.31q30.23-8 54.42-21.96 24.2-13.96 49.27-38.27L736.46-286l39.77-68-87.54-65.77q5-17.08 6.62-31.42 1.61-14.35 1.61-28.81 0-15.23-1.61-28.81-1.62-13.57-6.62-29.88L777.77-606 738-674l-102.08 42.77q-18.15-19.92-47.73-37.35-29.57-17.42-55.96-23.11L520-800h-79.77l-12.46 107.54q-30.23 6.46-55.58 20.81-25.34 14.34-50.42 39.42L222-674l-39.77 68L269-541.23q-5 13.46-7 29.23t-2 32.77q0 15.23 2 30.23t6.23 29.23l-86 65.77L222-286l99-42q23.54 23.77 48.88 38.12 25.35 14.34 57.12 22.34L440-160Zm38.92-220q41.85 0 70.93-29.08 29.07-29.07 29.07-70.92t-29.07-70.92Q520.77-580 478.92-580q-42.07 0-71.04 29.08-28.96 29.07-28.96 70.92t28.96 70.92Q436.85-380 478.92-380ZM480-480Z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -48,3 +48,14 @@
background-color: #eaeaea16; background-color: #eaeaea16;
border-color: #eaeaea; border-color: #eaeaea;
} }
.delete {
background-color: #de3535;
border-color: #e85f5f;
}
.delete:hover {
border-color: #fb7777;
filter: drop-shadow(#de3535 0 0 .6em);
}

View file

@ -1,52 +1,53 @@
// Preact
import { h } from 'preact'; import { h } from 'preact';
import { useState } from 'preact/hooks'; import { useState, useRef, useEffect } from 'preact/hooks';
import { searchMods } from '../services/api'; // Your API fetching function
import styles from './SearchBar.module.css'; // Optional: CSS Modules
function SearchBar({ onResults }) { // Images
const [searchTerm, setSearchTerm] = useState(''); import SearchIcon from '../../assets/search.svg'
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const handleInputChange = (event) => { // Styles
setSearchTerm(event.target.value); import styles from './search.module.css'; // Optional: CSS Modules
};
const handleSearch = async () => {
if (!searchTerm.trim()) {
onResults([]); // Clear results if search term is empty
return;
}
setLoading(true); function SearchBar({ onSearch }) {
setError(null);
try { const [search_input, setSearchInput] = useState('');
const results = await searchItems(searchTerm); const timeout_id = useRef(null);
onResults(results); // Pass the fetched results to the parent component
} catch (err) {
setError('Failed to fetch search results.');
onResults([]); // Clear results on error
} finally {
setLoading(false);
}
};
return ( const handleInputChange = (event) => {
<div className={styles.searchBarContainer}>
<input const new_search_input = event.target.value;
type="text" setSearchTerm(new_search_input);
placeholder="Search items..."
value={searchTerm} if (timeout_id.current) {
onChange={handleInputChange} clearTimeout(timeout_id.current);
className={styles.searchInput} }
/>
<button onClick={handleSearch} className={styles.searchButton} disabled={loading}> timeout_id.current = setTimeout(() => {
{loading ? 'Searching...' : 'Search'} onSearch(newSearchTerm);
</button> }, 500);
{error && <div className={styles.errorMessage}>{error}</div>} };
</div>
); useEffect(() => {
return () => {
if (timeout_id.current) {
clearTimeout(timeout_id.current);
}
};
}, []);
return (
<div className={styles.searchBarContainer}>
<img src={SearchIcon} className={styles.searchIcon}></img>
<input
type="text"
placeholder="Search"
value={search_input}
onChange={handleInputChange}
className={styles.searchBarInput}
/>
</div>
);
} }
export default SearchBar; export default SearchBar;

View file

@ -0,0 +1,42 @@
.searchBarContainer {
margin-bottom: 1rem;
/* padding: 0.5rem 1.2rem; */
box-sizing: border-box;
background-color: #242424;
border: transparent;
border: .1rem solid #3a3a3a;
border-radius: 20rem;
display: flex;
}
.searchBarInput {
width: 100%;
height: 100%;
padding: 0;
box-sizing: border-box;
margin: 0.5em;
font-size: 1rem;
font-family: 'IBM Plex Mono';
color: #ffffffc0;
background-color: transparent;
border: transparent;
}
.searchBarInput:focus {
outline: none;
color: #ffffff;
}
.searchIcon {
margin-left: 1em;
width: 1.6em;
user-select: none;
}

View file

@ -9,6 +9,7 @@
margin: 1rem; margin: 1rem;
width: 18rem; width: 18rem;
min-height: 40rem; min-height: 40rem;
padding: 1.3rem;
background-color: #1a1a1a; background-color: #1a1a1a;
color: #eaeaea; color: #eaeaea;
@ -16,4 +17,5 @@
border: #3a3a3a solid; border: #3a3a3a solid;
border-width: .1em; border-width: .1em;
border-radius: .5rem; border-radius: .5rem;
box-sizing: border-box;
} }

View file

@ -10,8 +10,8 @@ import styles from './navbar.module.css'
// Images // Images
import Settings from '../../assets/settings.svg' import Settings from '../../assets/settings.svg'
import Login from '../../assets/login_small.svg' import Login from '../../assets/login.svg'
import Account from '../../assets/account.svg' import Dashboard from '../../assets/dashboard.svg'
function NavBar({ children, className, ...rest}) { function NavBar({ children, className, ...rest}) {
@ -43,19 +43,19 @@ function NavBar({ children, className, ...rest}) {
<div className={styles.rightItems}> <div className={styles.rightItems}>
{/* Display login button, or profile picture if connected */} {/* Display login button, or dashboard if connected */}
{user ? ( {user ? (
<a className={styles.rightItem} href={'/users/' + user.username}> <a className={styles.rightItem} href={'/dashboard'}>
<img src={Account} width={'170%'} className={styles.profile}></img> <img src={Dashboard} className={styles.dashboard}></img>
</a> </a>
) : ( ) : (
<a className={styles.rightItem} href='/login'> <a className={styles.rightItem} href='/login'>
<img src={Login} width={'170%'} className={styles.login}></img> <img src={Login} className={styles.login}></img>
</a> </a>
) )
} }
<a className={styles.rightItem} href='/settings'> <a className={styles.rightItem} href='/settings'>
<img src={Settings} width={'170%'} className={styles.settings}></img> <img src={Settings} className={styles.settings}></img>
</a> </a>
</div> </div>

View file

@ -18,7 +18,8 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
padding-left: 2em; padding: 1.3em;
box-sizing: border-box;
} }
.leftItem { .leftItem {
@ -45,7 +46,7 @@
/* WIP */ /* WIP */
.rightItem { .rightItem {
margin: 0 1em; margin: 0 .5em;
user-select: none; user-select: none;
@ -54,28 +55,31 @@
.rightItems { .rightItems {
margin-left: auto; margin-left: auto;
margin-right: 1em; height: 100%;
margin-top: .3em; /* margin-right: 1em; */
/* margin-top: .3em; */
display: flex; display: flex;
} }
.settings { .settings {
height: 100%;
border-radius: 100rem; border-radius: 100rem;
transition: 600ms; transition: 600ms;
} }
.profile { .dashboard {
border-radius: 100rem; height: 100%;
border-radius: .5rem;
transition: 300ms; transition: 300ms;
} }
.login { .login {
height: 100%;
border-radius: .5rem; border-radius: .5rem;
transition: 600ms; transition: 600ms;
margin-top: .06em; margin-top: .06em;
height: 80%;
} }
.login:hover { .login:hover {
@ -87,6 +91,6 @@
transform: rotate(180deg); transform: rotate(180deg);
} }
.profile:hover { .dashboard:hover {
background-color: #eaeaea10; background-color: #eaeaea10;
} }

View file

@ -26,7 +26,7 @@ function AboutPage() {
<div class='mainText'> <div class='mainText'>
WF radio is a platform for all your personnal mods and modpacks. 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. 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. It's meant for your personnal works that you don't want to publish on the these 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). Don't hesitate to learn more about the project with the link below (not here yet).
</div> </div>
</> </>

View file

@ -1 +1,67 @@
// TODO import { h } from 'preact';
import { useState } from 'preact/hooks';
// Components
import Button from '../components/Buttons/button'
// Images
import Logo from '../assets/logo.png'
import Add from '../assets/add.svg'
// Styles
import styles from '../styles/dashboard.module.css'
function DashboardPage() {
const handleCreate = () => {
window.location.href('/create/mod');
}
return (
<>
<a href='/' target="_blank">
<img src={Logo} class="logoSmall img" alt="WF" />
<p class="logoSmall text"> dashboard </p>
</a>
<div class='container'>
<div className={styles.category}>
<p className={styles.title}>
Favorites
</p>
<div className={styles.tiles}>
<div className={styles.emptyTile}>
<p className={styles.emptyTileText}>
You have no favorites for the moment
</p>
</div>
</div>
</div>
<div className={styles.category}>
<p className={styles.title}>
Your creations
</p>
<div className={styles.tiles}>
<a className={styles.emptyTile} href='/create/mod'>
<p className={styles.emptyTileText}>
<img src={Add} ></img>
</p>
</a>
</div>
</div>
<div className={styles.toolbar}>
<div className={styles.toolbarRightItems}>
<Button
className={styles.createButton}
href='/create/mod'
>
Create
</Button>
</div>
</div>
</div>
</>
);
}
export default DashboardPage;

View file

@ -21,7 +21,7 @@ function HomePage() {
<p class="logo text"> radio </p> <p class="logo text"> radio </p>
</a> </a>
<div class='title'>An open place for mods</div> <div class='title'>An open place for mods</div>
<Button className='start-button' href='/mods'>Get started</Button> <Button className='start-button' href='/mods'>Start exploring</Button>
<div class='background'></div> <div class='background'></div>
<div class='halo'></div> <div class='halo'></div>
</> </>

View file

@ -15,7 +15,7 @@ import Button from '../components/Buttons/button';
import InputField from '../components/Fields/input_field'; import InputField from '../components/Fields/input_field';
// Functions // Functions
import { login } from '../services/auth'; import { login } from '../services/users';
function LoginPage() { function LoginPage() {
@ -129,7 +129,8 @@ function LoginPage() {
type='submit' type='submit'
disabled={loginStatus === 'logging in'} disabled={loginStatus === 'logging in'}
> >
{loginStatus === 'logging in' ? 'Logging in' : 'Login'} {loginStatus === 'logging in' ? 'Logging in' :
loginStatus === 'success' ? 'Success' : 'Login'}
</Button> </Button>
<Button <Button
className={styles.loginButton} className={styles.loginButton}

View file

@ -7,6 +7,7 @@ import { listMods } from '../services/mods';
// Components // Components
import FiltersPanel from '../components/Filters/panel' import FiltersPanel from '../components/Filters/panel'
import SearchBar from '../components/Fields/search';
// Images // Images
import logo from '../assets/logo.png' import logo from '../assets/logo.png'
@ -18,18 +19,32 @@ import RowCard from '../components/Cards/row';
function ModsPage() { function ModsPage() {
// UseState // List mods
const [mods, setMods] = useState([]); const [mods, setMods] = useState([]);
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [error, setError] = useState(null) const [error, setError] = useState(null)
// Filters
const [search_input, setSearchInput] = useState('');
const [selected_categories, setSelectedCategories] = useState([]);
const handleSearch = (new_search_input) => {
setSearchInput(new_search_input);
};
// UseEffect // UseEffect
useEffect(() => { useEffect(() => {
async function loadItems() { async function loadItems() {
setLoading(true); setLoading(true);
setError(false); setError(false);
try { try {
const fetched_mods = await listMods(); const filters = {
search: search_input,
categories: selected_categories
};
const fetched_mods = await listMods(filters);
setMods(fetched_mods); setMods(fetched_mods);
} catch (err) { } catch (err) {
setError(err.message); setError(err.message);
@ -48,7 +63,10 @@ function ModsPage() {
<img src={logo} class="logo img" alt="WF" /> <img src={logo} class="logo img" alt="WF" />
<p class="logo text"> mods </p> <p class="logo text"> mods </p>
</a> </a>
<FiltersPanel></FiltersPanel> <FiltersPanel>
<SearchBar onSearch={handleSearch} />
</FiltersPanel>
</> </>
); );
@ -91,7 +109,7 @@ function ModsPage() {
{mods.map((mod) => { {mods.map((mod) => {
console.debug(mod.name); console.debug(mod.name);
// 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} />
})} })}
</div> </div>
</> </>

View file

@ -16,7 +16,7 @@ import Button from '../components/Buttons/button';
import InputField from '../components/Fields/input_field'; import InputField from '../components/Fields/input_field';
// Functions // Functions
import { register } from '../services/auth'; import { register } from '../services/users';
function RegisterPage() { function RegisterPage() {

View file

@ -4,6 +4,12 @@ import { useState, useEffect } from 'preact/hooks'; // If you need state for set
import Cookies from 'js-cookie' import Cookies from 'js-cookie'
import { jwtDecode } from 'jwt-decode' import { jwtDecode } from 'jwt-decode'
// Functions
import { deleteUser } from '../services/users';
// Components
import Button from '../components/Buttons/button';
// Styles // Styles
import styles from '../styles/settings.module.css' import styles from '../styles/settings.module.css'
@ -16,12 +22,17 @@ function SettingsPage() {
const [theme, setTheme] = useState('dark'); const [theme, setTheme] = useState('dark');
const [notificationsEnabled, setNotificationsEnabled] = useState(true); const [notificationsEnabled, setNotificationsEnabled] = useState(true);
const [user, setUser] = useState(null) const [user, setUser] = useState(null)
const [tab, setTab] = useState('global');
useEffect( () => { useEffect( () => {
const token = Cookies.get('authToken'); const token = Cookies.get('authToken');
if (token) { if (token) {
setUser(token); const decoded_token = jwtDecode(token);
if (decoded_token && decoded_token.username) {
setUser(decoded_token.username);
}
} }
}, },
[]) [])
@ -36,7 +47,18 @@ function SettingsPage() {
// Save notification preference // Save notification preference
}; };
const logout = () => { const handleUserDeletion = async () => {
try {
await deleteUser(user);
handleLogout();
} catch (error) {
console.error("Couldn't delete account: ", error);
}
}
const handleLogout = () => {
Cookies.remove('authToken'); Cookies.remove('authToken');
location.replace('/'); location.replace('/');
} }
@ -48,15 +70,15 @@ function SettingsPage() {
<p className={styles.logoTitle}> settings </p> <p className={styles.logoTitle}> settings </p>
</a> </a>
<div className={styles.tabsContainer}> <div className={styles.tabsContainer}>
<a href='/settings'> <a onClick={() => {setTab('global')}}>
<p className={styles.tab}>Global</p> <p className={styles.tab}>Global</p>
</a> </a>
{user ? ( {user ? (
<> <>
<a href='/notfound'> <a onClick={() => {setTab('user')}}>
<p className={styles.tab}>User</p> <p className={styles.tab}>User</p>
</a> </a>
<a onClick={() => {logout()}}> <a onClick={() => {handleLogout()}}>
<p className={`${styles.tab} ${styles.logout}`}>Logout</p> <p className={`${styles.tab} ${styles.logout}`}>Logout</p>
</a> </a>
</> </>
@ -66,23 +88,46 @@ function SettingsPage() {
</div> </div>
<div className='container'> <div className='container'>
<div> { tab === 'global' ? (
<label htmlFor="theme">Theme:</label> <>
<select id="theme" value={theme} onChange={handleThemeChange}> <p>
<option value="light">Light</option> <label htmlFor="theme">Theme:</label>
<option value="dark">Dark</option> <select id="theme" value={theme} onChange={handleThemeChange}>
</select> <option value="light">Light</option>
</div> <option value="dark">Dark</option>
<div> </select>
<label> </p>
Enable Notifications: <p>
<input <label>
type="checkbox" Enable Notifications:
checked={notificationsEnabled} <input
onChange={handleNotificationsChange} type="checkbox"
/> checked={notificationsEnabled}
</label> onChange={handleNotificationsChange}
</div> />
</label>
</p>
</>
) :
tab === 'user' ? (
<>
<div className={styles.category}>
<p className={styles.title}>Danger zone</p>
<Button
variant='delete'
style='font-size: 1.2rem'
onClick={handleUserDeletion}
>
Delete account
</Button>
</div>
</>
) : (
<>
</>
)
}
</div> </div>
</> </>
); );

View file

@ -30,7 +30,7 @@ export async function createMod(mod_data) {
} }
export async function listMods() { export async function listMods(filters) {
try { try {
const response = await fetch(`${API_BASE_URL}/list/mods`); const response = await fetch(`${API_BASE_URL}/list/mods`);
if (!response.ok) { if (!response.ok) {

View file

@ -25,8 +25,6 @@ export async function login(username, password) {
export async function register(user_data) { export async function register(user_data) {
try { try {
console.debug(user_data);
console.debug(JSON.stringify(user_data));
const response = await fetch(`${API_BASE_URL}/users`, { const response = await fetch(`${API_BASE_URL}/users`, {
method: 'POST', method: 'POST',
headers: { headers: {
@ -44,3 +42,22 @@ export async function register(user_data) {
throw error; throw error;
} }
} }
export async function deleteUser(username) {
try {
const authToken = Cookies.get('authToken');
const response = await fetch(`${API_BASE_URL}/users/${username}`, {
method: 'DELETE',
headers: {
'Authorization': authToken,
}});
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);
throw error;
}
}

View file

@ -24,6 +24,7 @@ body {
* { * {
transition: 200ms; transition: 200ms;
-webkit-user-drag: none;
} }
@ -31,6 +32,12 @@ a {
text-decoration: none; text-decoration: none;
} }
::selection {
color: #ffffff;
background: #353be5;
}
/* Logo */ /* Logo */
.logo.img { .logo.img {
@ -87,6 +94,8 @@ a {
user-select: none; user-select: none;
} }
/* Titles */
h1 { h1 {
position: absolute; position: absolute;
left: 0rem; left: 0rem;
@ -101,6 +110,7 @@ h1 {
font-size: 3em; font-size: 3em;
} }
/* Containers */
.content-container { .content-container {
@ -137,6 +147,8 @@ h1 {
border-width: .1em; border-width: .1em;
border-radius: .5rem; border-radius: .5rem;
overflow: scroll;
} }
.loadingContent { .loadingContent {

View file

@ -0,0 +1,71 @@
.category {
margin-top: 0;
}
.title {
margin-top: 0;
color: #eaeaea;
font-size: 1.6em;
font-weight: 600;
user-select: none;
}
.tiles {
height: 13rem;
margin-bottom: 3rem;
overflow-x: scroll;
overflow-y: visible;
display: flex;
gap: 2em;
}
.toolbar {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 5rem;
padding: .5rem;
/* background-color: #9a9a9a; */
}
.toolbarRightItems {
position: absolute;
right: 1rem;
bottom: 1rem;
}
.createButton {
font-size: 1.4rem;
}
.emptyTile {
height: 99%;
width: 22em;
align-items: center;
display: flex;
background-color: #141414;
border: #3a3a3a .1rem solid;
border-radius: 1rem;
}
.emptyTileText {
padding: 1em;
width: 100%;
color: #9a9a9a;
text-align: center;
vertical-align: middle;
user-select: none;
}

View file

@ -84,6 +84,6 @@
color: #eaeaead0; color: #eaeaead0;
font-size: 1.5em; font-size: 1.5em;
text-align: justify; text-align: center;
} }

View file

@ -59,6 +59,7 @@
.tab:hover { .tab:hover {
background-color: #eaeaea20; background-color: #eaeaea20;
cursor: pointer;
} }
.tab:active { .tab:active {
@ -74,3 +75,14 @@
color: #eaeaea; color: #eaeaea;
background-color: #bc3939; background-color: #bc3939;
} }
.title {
font-size: 1.4em;
font-weight: 600;
margin-top: 0;
margin-bottom: 2em;
}
.category {
background: none; /*Only to remove the empty warning for the moment */
}