diff --git a/frontend/config/config.json b/frontend/config/config.json new file mode 100644 index 0000000..00513e4 --- /dev/null +++ b/frontend/config/config.json @@ -0,0 +1,10 @@ +{ + "port": 8100, + + "backend": { + "address": "localhost", + "port": 8000, + "protocol": "http", + "path": "/" + } +} \ No newline at end of file diff --git a/frontend/src/app.jsx b/frontend/src/app.jsx index 5d08b7d..816f90e 100644 --- a/frontend/src/app.jsx +++ b/frontend/src/app.jsx @@ -7,6 +7,8 @@ import { Router } from 'preact-router'; // Pages import HomePage from './pages/home'; import AboutPage from './pages/about'; +import NotFoundPage from './pages/not_found'; +import ComingSoonPage from './pages/coming_soon'; import ModsPage from './pages/mods'; import ModPage from './pages/mod_page' @@ -48,6 +50,9 @@ export function App() { + + + diff --git a/frontend/src/components/Fields/search.jsx b/frontend/src/components/Fields/search.jsx index e69de29..fb6eff1 100644 --- a/frontend/src/components/Fields/search.jsx +++ b/frontend/src/components/Fields/search.jsx @@ -0,0 +1,52 @@ +import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import { searchMods } from '../services/api'; // Your API fetching function +import styles from './SearchBar.module.css'; // Optional: CSS Modules + +function SearchBar({ onResults }) { + const [searchTerm, setSearchTerm] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleInputChange = (event) => { + setSearchTerm(event.target.value); + }; + + const handleSearch = async () => { + if (!searchTerm.trim()) { + onResults([]); // Clear results if search term is empty + return; + } + + setLoading(true); + setError(null); + + try { + const results = await searchItems(searchTerm); + 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 ( +
+ + + {error &&
{error}
} +
+ ); +} + +export default SearchBar; \ No newline at end of file diff --git a/frontend/src/components/NavBar/navbar.module.css b/frontend/src/components/NavBar/navbar.module.css index 954bc7d..2d362d9 100644 --- a/frontend/src/components/NavBar/navbar.module.css +++ b/frontend/src/components/NavBar/navbar.module.css @@ -39,6 +39,9 @@ .leftItem:hover { background-color: #eaeaea10; } +.leftItem:active { + background-color: #eaeaea20; +} /* WIP */ .rightItem { diff --git a/frontend/src/pages/about.jsx b/frontend/src/pages/about.jsx index aa72e9d..419a0eb 100644 --- a/frontend/src/pages/about.jsx +++ b/frontend/src/pages/about.jsx @@ -16,7 +16,7 @@ function AboutPage() { return ( <> - + diff --git a/frontend/src/pages/coming_soon.jsx b/frontend/src/pages/coming_soon.jsx new file mode 100644 index 0000000..198b13c --- /dev/null +++ b/frontend/src/pages/coming_soon.jsx @@ -0,0 +1,29 @@ +// Functions +import { h } from 'preact'; + +// Images +import logo from '../assets/logo.png' + +// Styles +import '../styles/home.css' + +// Components +import Button from '../components/Buttons/button'; + + +function ComingSoonPage() { + + return ( + <> + + + + +
Coming soon™
+
+
+ + ); +} + +export default ComingSoonPage; \ No newline at end of file diff --git a/frontend/src/pages/dashboard.jsx b/frontend/src/pages/dashboard.jsx new file mode 100644 index 0000000..0ffdd02 --- /dev/null +++ b/frontend/src/pages/dashboard.jsx @@ -0,0 +1 @@ +// TODO \ No newline at end of file diff --git a/frontend/src/pages/home.jsx b/frontend/src/pages/home.jsx index 072aeb5..446d0ae 100644 --- a/frontend/src/pages/home.jsx +++ b/frontend/src/pages/home.jsx @@ -16,7 +16,7 @@ function HomePage() { return ( <> - + diff --git a/frontend/src/pages/login.jsx b/frontend/src/pages/login.jsx index 423fc91..42be74f 100644 --- a/frontend/src/pages/login.jsx +++ b/frontend/src/pages/login.jsx @@ -2,6 +2,7 @@ import { h } from 'preact'; import { useState } from 'preact/hooks'; import Cookies from 'js-cookie'; +import { jwtDecode } from 'jwt-decode' // Images import logo from '../assets/logo.png' @@ -14,7 +15,7 @@ import Button from '../components/Buttons/button'; import InputField from '../components/Fields/input_field'; // Functions -import { login } from '../services/api'; +import { login } from '../services/auth'; function LoginPage() { @@ -41,8 +42,8 @@ function LoginPage() { }; const handleSubmit = async (event) => { - event.preventDefault(); // Prevent the default form submission - setFieldErrors({ name: null, email: null, message: null }); + event.preventDefault(); + setFieldErrors({ username: null, password: null}); setLoginStatus('logging in'); @@ -53,8 +54,12 @@ function LoginPage() { setLoginStatus('success'); if (response && response.token) { + const decoded_token = jwtDecode(response.token); + if (!decoded_token) { + throw new Error("Couldn't decode token"); + } Cookies.set('authToken', response.token, { - expires: 30, + expires: decoded_token.exp, //TODO not sure if it's the right value path: '/', secure: true, // only send over https sameSite: 'strict' diff --git a/frontend/src/pages/mod_creation.jsx b/frontend/src/pages/mod_creation.jsx index 91d219e..2d08fe7 100644 --- a/frontend/src/pages/mod_creation.jsx +++ b/frontend/src/pages/mod_creation.jsx @@ -3,7 +3,7 @@ import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; // Functions -import { createMod } from '../services/api'; +import { createMod } from '../services/mods'; // Components import InputField from '../components/Fields/input_field' diff --git a/frontend/src/pages/mod_page.jsx b/frontend/src/pages/mod_page.jsx index b49aa88..2f47bb4 100644 --- a/frontend/src/pages/mod_page.jsx +++ b/frontend/src/pages/mod_page.jsx @@ -3,7 +3,7 @@ import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; // Functions -import { fetchMod } from '../services/api'; +import { getMod } from '../services/mods'; // Components @@ -28,7 +28,7 @@ function ModPage({name}) { setLoading(true); setError(false); try { - const fetched_mod = await fetchMod(name); + const fetched_mod = await getMod(name); setMod(fetched_mod); } catch (err) { setError(err.message); @@ -39,23 +39,49 @@ function ModPage({name}) { loadItems(); }, []); // <-- Tells useEffect to run once after render + + const base_page = ( + <> + + + + + + ); if (loading) { // TODO replace by loading screen - return
Loading mod
+ return ( + <> + {base_page} +
+
+

Loading...

+
+
+
+ + ); } if (error) { // TODO replace by popup - return
Couldn't load mod: {error}
+ return ( + <> + {base_page} +
+
+

Couldn't load this mod

+

{error}

+
+
+
+ + ); } return ( <> - - - - - + {base_page}
diff --git a/frontend/src/pages/modpacks.jsx b/frontend/src/pages/modpacks.jsx index dda30ed..6d872ff 100644 --- a/frontend/src/pages/modpacks.jsx +++ b/frontend/src/pages/modpacks.jsx @@ -16,7 +16,7 @@ function ModpacksPage() { return ( <> - + WF

modpacks

diff --git a/frontend/src/pages/mods.jsx b/frontend/src/pages/mods.jsx index 1cb3fdb..0d916fe 100644 --- a/frontend/src/pages/mods.jsx +++ b/frontend/src/pages/mods.jsx @@ -3,7 +3,7 @@ import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; // Functions -import { fetchMods } from '../services/api'; +import { listMods } from '../services/mods'; // Components import FiltersPanel from '../components/Filters/panel' @@ -29,7 +29,7 @@ function ModsPage() { setLoading(true); setError(false); try { - const fetched_mods = await fetchMods(); + const fetched_mods = await listMods(); setMods(fetched_mods); } catch (err) { setError(err.message); @@ -41,22 +41,49 @@ function ModsPage() { loadItems(); }, []); // <-- Tells useEffect to run once after render + // Base page + const base_page = ( + <> + + + + + + + ); + if (loading) { // TODO replace by loading screen - return
Loading mods
+ return ( + <> + {base_page} +
+
+ Loading mods +
+
+ + + ); } if (error) { // TODO replace by popup - return
Couldn't load mods: {error}
+ return ( + <> + {base_page} +
+
+ Couldn't load mods: {error} +
+
+ + + ); } return ( <> - - - - - + {base_page}
{/* Test card */} {/* */} diff --git a/frontend/src/pages/not_found.jsx b/frontend/src/pages/not_found.jsx new file mode 100644 index 0000000..69bdaf3 --- /dev/null +++ b/frontend/src/pages/not_found.jsx @@ -0,0 +1,30 @@ +// Functions +import { h } from 'preact'; + +// Images +import logo from '../assets/logo.png' + +// Styles +import '../styles/home.css' + +// Components +import Button from '../components/Buttons/button'; + + +function NotFoundPage() { + + return ( + <> + + + + +
404
+
Page not found
+
+
+ + ); +} + +export default NotFoundPage; \ No newline at end of file diff --git a/frontend/src/pages/register.jsx b/frontend/src/pages/register.jsx index e2b7bdd..f065661 100644 --- a/frontend/src/pages/register.jsx +++ b/frontend/src/pages/register.jsx @@ -1,8 +1,12 @@ -// Functions +// Preact import { h } from 'preact'; +import { useState } from 'preact/hooks'; +import Cookies from 'js-cookie'; +import { jwtDecode } from 'jwt-decode' // Images -import logo from '../assets/logo.png' +import Logo from '../assets/logo.png' +import Account from '../assets/account.svg' // Styles import styles from '../styles/login.module.css' @@ -11,55 +15,128 @@ import styles from '../styles/login.module.css' import Button from '../components/Buttons/button'; import InputField from '../components/Fields/input_field'; +// Functions +import { register } from '../services/auth'; + function RegisterPage() { - const username_input= ""; - const display_name_input= ""; - const email_input= ""; - const password_input= ""; + // useState + const [username, setUsername] = useState(''); + const [display_name, setDisplayName] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [registerStatus, setRegisterStatus] = useState(null); // To track success/error + const [fieldErrors, setFieldErrors] = useState({ + username: null, + display_name: null, + email: null, + password: null + }); + + const handleUsernameChange = (event) => { + setUsername(event.target.value); + // Reset error + setFieldErrors(prevErrors => ({ ...prevErrors, username: null })); + }; + + const handlePasswordChange = (event) => { + setPassword(event.target.value); + // Reset error + setFieldErrors(prevErrors => ({ ...prevErrors, password: null })); + }; + + const handleEmailChange = (event) => { + setEmail(event.target.value); + // Reset error + setFieldErrors(prevErrors => ({ ...prevErrors, email: null })); + }; + + const handleDisplayNameChange = (event) => { + setDisplayName(event.target.value); + // Reset error + setFieldErrors(prevErrors => ({ ...prevErrors, display_name: null })); + }; + + const handleSubmit = async (event) => { + event.preventDefault(); + setFieldErrors({ username: null, password: null, email: null, display_name: null}); + + setRegisterStatus('registering'); + + try { + const response = await register({username: username, + display_name: display_name, + email: email, + password: password}); + + console.debug('Registered successfully:', response); + setRegisterStatus('success'); + + window.location.replace("/login"); + //TODO success screen + + } catch (error) { + + console.error('Register failed:', error); + setRegisterStatus('error'); + + //TODO handle different codes differently + setFieldErrors({ + username: ' ', + email: ' ', + display_name: ' ', + password: 'Error' + }); + } + finally { + setRegisterStatus(null); + } + }; return ( <> - - - + + +
- -
+ + + {/* */} +
{/* username */} - + {/* display_name */} - + {/* email */} @@ -69,18 +146,22 @@ function RegisterPage() {
-
diff --git a/frontend/src/pages/settings.jsx b/frontend/src/pages/settings.jsx index 9d8f28b..302bd6c 100644 --- a/frontend/src/pages/settings.jsx +++ b/frontend/src/pages/settings.jsx @@ -44,20 +44,22 @@ function SettingsPage() { return ( <> - WF -

settings

+ WF +

settings

Global

- -

User

-
{user ? ( - {logout()}}> -

Logout

-
+ <> + +

User

+
+ {logout()}}> +

Logout

+
+ ) : '' } diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js deleted file mode 100644 index 55291bc..0000000 --- a/frontend/src/services/api.js +++ /dev/null @@ -1,66 +0,0 @@ -const API_BASE_URL = 'http://localhost:8000'; - -export async function fetchMods() { - try { - const response = await fetch(`${API_BASE_URL}/list/mods`); - 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 []; - } -} - -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', - headers: { - 'Content-Type': 'application/json', - }, - 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); - throw error; - } -} - -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; - } -} \ No newline at end of file diff --git a/frontend/src/services/auth.js b/frontend/src/services/auth.js new file mode 100644 index 0000000..9ab7f1e --- /dev/null +++ b/frontend/src/services/auth.js @@ -0,0 +1,46 @@ +import Cookies from 'js-cookie'; + +const API_BASE_URL = 'http://localhost:8000'; //TODO use config manager instead + + +export async function login(username, password) { + try { + const response = await fetch(`${API_BASE_URL}/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + 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); + throw error; + } +} + +export async function register(user_data) { + try { + console.debug(user_data); + console.debug(JSON.stringify(user_data)); + const response = await fetch(`${API_BASE_URL}/users`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(user_data) + }); + 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; + } +} \ No newline at end of file diff --git a/frontend/src/services/config_manager.js b/frontend/src/services/config_manager.js new file mode 100644 index 0000000..ba822a3 --- /dev/null +++ b/frontend/src/services/config_manager.js @@ -0,0 +1,70 @@ +// --- Define constants --- + +// Imports +import fs from "fs"; +import path from "path"; +import { version } from "../../package.json"; + + +// Var decalaration +const config_folder = "config"; +const config_file_name = "config.json" + +// Global variables +let config = {}; +let backend_url = ""; + +// --- Default config --- + +const default_config = { + + "port": 8100, + + "backend": { + "address": "localhost", + "port": 8000, + "protocol": "http", + "path": "/" + } +} + +function loadConfig() { + + let user_config; + + // Parse + try { + // Get user config + user_config = JSON.parse(fs.readFileSync(path.resolve(path.join(config_folder, config_file_name)))); + + // Merge default and user configs (default values) + config = { ...default_config, ...user_config }; + + backend_url = config.backend.protocol + '://' + + config.backend.address + ':' + + config.backend.port + + config.backend.path; + //TODO test url + } + catch (err) { + // Error messages + console.debug("Error:", err) + console.error("Error loading configuration, using the default settings"); + console.debug("Search path:", path.resolve("./")); + console.debug("Config file:", path.resolve(path.join(config_folder, config_file_name))) + + config = default_config; + } + + return config; + +} + +function getBackendUrl() { + if (backend_url) { + return backend_url; + } else { + throw new Error("Backend url is not valid"); + } + +} \ No newline at end of file diff --git a/frontend/src/services/mods.js b/frontend/src/services/mods.js new file mode 100644 index 0000000..9fbef21 --- /dev/null +++ b/frontend/src/services/mods.js @@ -0,0 +1,60 @@ +import Cookies from 'js-cookie'; + +const API_BASE_URL = 'http://localhost:8000'; //TODO use config manager instead + + +export async function createMod(mod_data) { + try { + const auth_token = Cookies.get('authToken'); + + const response = await fetch(`${API_BASE_URL}/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': auth_token, + }, + body: JSON.stringify({...mod_data}) + }); + + 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; + } +} + + +export async function listMods() { + try { + const response = await fetch(`${API_BASE_URL}/list/mods`); + 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; + } +} + + +export async function getMod(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); + throw error; + } +} \ No newline at end of file diff --git a/frontend/src/styles/app.css b/frontend/src/styles/app.css index 8584284..b542145 100644 --- a/frontend/src/styles/app.css +++ b/frontend/src/styles/app.css @@ -139,6 +139,10 @@ h1 { } +.loadingContent { + color: #7a7a7a; +} + /* Page transitions */ .page { diff --git a/frontend/src/styles/home.css b/frontend/src/styles/home.css index 976b1b1..8f922e6 100644 --- a/frontend/src/styles/home.css +++ b/frontend/src/styles/home.css @@ -32,6 +32,38 @@ letter-spacing: 0.1rem; } +.bigTitle { + position: fixed; + top: 26rem; + left: 50%; + transform: translate(-50%, -50%); /*Center (compensate size)*/ + z-index: -2; + + color: #eaeaea; + text-align: center; + font-family: "Inter"; + font-weight: 600; + font-size: 8rem; + user-select: none; + letter-spacing: 0.1rem; +} + +.subtitle { + position: fixed; + top: 34rem; + left: 50%; + transform: translate(-50%, -50%); /*Center (compensate size)*/ + z-index: -2; + + color: #eaeaea; + text-align: center; + font-family: "Inter"; + font-weight: 600; + font-size: 500%; + user-select: none; + letter-spacing: 0.1rem; +} + .start-button { position: fixed; top: 48rem; diff --git a/frontend/src/styles/settings.module.css b/frontend/src/styles/settings.module.css index 1d51359..df023ba 100644 --- a/frontend/src/styles/settings.module.css +++ b/frontend/src/styles/settings.module.css @@ -1,3 +1,29 @@ +.logo { + position: absolute; + top: 1.6rem; + left: 2rem; + height: 6em; + + padding: 1.5em; + + user-select: none; +} + +.logoTitle { + position: absolute; + left: 10rem; + top: 4rem; + + margin: 0; + + color: #eaeaea; + font-family: Inter; + font-weight: 600; + font-size: 2.5em; + + user-select: none; +} + .tabsContainer { position: absolute; left: 4rem; @@ -32,7 +58,11 @@ } .tab:hover { - background-color: #3a3a3a; + background-color: #eaeaea20; +} + +.tab:active { + background-color: #eaeaea30; } .logout {