feat(frontend): added frontend
- Router: client side route handling with client side rendering - Toast: rought Toast handling for better UX and messaging - Auth: single point of truth for the Logged in user This commit doesnt not include the openapi generated code
This commit is contained in:
parent
0db41a440d
commit
08c910c193
28 changed files with 1994 additions and 0 deletions
0
frontend/src/pages/about/about.html
Normal file
0
frontend/src/pages/about/about.html
Normal file
12
frontend/src/pages/about/about.ts
Normal file
12
frontend/src/pages/about/about.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { addRoute, setTitle } from "@app/routing";
|
||||
import page from './about.html?raw'
|
||||
|
||||
|
||||
async function route(_url: string, _args: { [k: string]: string }): Promise<string> {
|
||||
setTitle('About us')
|
||||
return page;
|
||||
}
|
||||
|
||||
|
||||
|
||||
addRoute('/', route)
|
||||
5
frontend/src/pages/chat/chat.ts
Normal file
5
frontend/src/pages/chat/chat.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
import { addRoute, type RouteHandlerParams } from "@app/routing";
|
||||
|
||||
addRoute('/chat', function (_url: string, _args: RouteHandlerParams) {
|
||||
return "this is the chat page !"
|
||||
})
|
||||
8
frontend/src/pages/index.ts
Normal file
8
frontend/src/pages/index.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { setTitle, handleRoute } from '@app/routing';
|
||||
import './root/root.ts'
|
||||
import './chat/chat.ts'
|
||||
import './login/login.ts'
|
||||
|
||||
// ---- Initial load ----
|
||||
setTitle("");
|
||||
handleRoute();
|
||||
41
frontend/src/pages/login/login.html
Normal file
41
frontend/src/pages/login/login.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<div class="grid h-full place-items-center">
|
||||
<div class="bg-white shadow-lg rounded-2xl p-8 w-full max-w-md">
|
||||
<h1 class="text-2xl font-semibold text-center mb-6 text-gray-800">Welcome to <span>ft boules</span></h1>
|
||||
|
||||
<button id="bGuestLogin"
|
||||
class="w-full bg-gray-600 text-white font-medium py-2 rounded-xl hover:bg-gray-700 transition">
|
||||
Login as Guest
|
||||
</button>
|
||||
|
||||
<form class="space-y-5 pt-3" id="login-form">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Username</label>
|
||||
<input type="text" placeholder="Enter your username" name="login"
|
||||
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Password</label>
|
||||
<input type="password" placeholder="Enter your password" name="password"
|
||||
class="w-full px-4 py-2 border border-gray-300 text-gray-700 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500" />
|
||||
</div>
|
||||
|
||||
<button type="submit"
|
||||
class="w-full bg-blue-600 text-white font-medium py-2 rounded-xl hover:bg-blue-700 transition">
|
||||
Sign In
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-4">
|
||||
Don’t have an account?
|
||||
<a href="/signin" class="text-blue-600 hover:underline">Sign up</a>
|
||||
</p>
|
||||
|
||||
<p class="text-center text-sm text-gray-500 mt-4">
|
||||
You can also login with
|
||||
</p>
|
||||
|
||||
<div id="otherLogin" class="pt-5 space-y-5 grid grid-cols-2 gap-4">
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
152
frontend/src/pages/login/login.ts
Normal file
152
frontend/src/pages/login/login.ts
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
import { addRoute, setTitle, type RouteHandlerParams, type RouteHandlerReturn } from "@app/routing";
|
||||
import { showError, showInfo, showSuccess } from "@app/toast";
|
||||
import authHtml from './login.html?raw';
|
||||
import client from '@app/api'
|
||||
import { updateUser } from "@app/auth";
|
||||
|
||||
|
||||
type Providers = {
|
||||
name: string,
|
||||
display_name: string,
|
||||
icon_url?: string,
|
||||
color?: { default: string, hover: string },
|
||||
};
|
||||
|
||||
function handleLogin(_url: string, _args: RouteHandlerParams): RouteHandlerReturn {
|
||||
setTitle('Login')
|
||||
return {
|
||||
html: authHtml, postInsert: async (app) => {
|
||||
const fLogin = document.querySelector<HTMLFormElement>('form#login-form');
|
||||
if (fLogin === null)
|
||||
return showError('Error while rendering the page: no form found');
|
||||
showSuccess('got the form !')
|
||||
fLogin.addEventListener('submit', async function(e: SubmitEvent) {
|
||||
e.preventDefault();
|
||||
let form = e.target as (HTMLFormElement | null)
|
||||
if (form === null)
|
||||
return showError('Failed to send form...');
|
||||
let formData = Object.fromEntries((new FormData(form)).entries());
|
||||
if (!('login' in formData) || typeof formData['login'] !== 'string' || (formData['login'] as string).length === 0)
|
||||
return showError('Please enter a Login');
|
||||
if (!('password' in formData) || typeof formData['password'] !== 'string' || (formData['password'] as string).length === 0)
|
||||
return showError('Please enter a Password');
|
||||
try {
|
||||
const res = await client.login({ loginRequest: { name: formData.login, password: formData.password } });
|
||||
switch (res.kind) {
|
||||
case 'success': {
|
||||
document.cookie = `token=${res.payload.token}`;
|
||||
let user = await updateUser();
|
||||
if (user === null)
|
||||
return showError('Failed to get user: no user ?');
|
||||
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`);
|
||||
break;
|
||||
}
|
||||
case 'otpRequired': {
|
||||
showInfo('Got ask OTP, not yet implemented');
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
showError(`Failed to login: ${res.msg}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Login error:", e);
|
||||
showError('Failed to login: Unknown error');
|
||||
}
|
||||
});
|
||||
|
||||
const bLoginAsGuest = document.querySelector<HTMLButtonElement>('#bGuestLogin');
|
||||
bLoginAsGuest?.addEventListener('click', async () => {
|
||||
try {
|
||||
const res = await client.guestLogin();
|
||||
switch (res.kind) {
|
||||
case 'success': {
|
||||
document.cookie = `token=${res.payload.token}`;
|
||||
let user = await updateUser();
|
||||
if (user === null)
|
||||
return showError('Failed to get user: no user ?');
|
||||
setTitle(`Welcome ${user.guest ? '[GUEST] ' : ''}${user.name}`);
|
||||
break;
|
||||
}
|
||||
case 'failed': {
|
||||
showError(`Failed to login: ${res.msg}`);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Login error:", e);
|
||||
showError('Failed to login: Unknown error');
|
||||
}
|
||||
});
|
||||
|
||||
const dOtherLoginArea = document.querySelector<HTMLDivElement>('#otherLogin');
|
||||
if (dOtherLoginArea) {
|
||||
let styleSheetElement = document.createElement('style');
|
||||
styleSheetElement.innerText = "";
|
||||
// TODO: fetch all the providers from an API ?
|
||||
const providers: Providers[] = [
|
||||
{ name: 'discord', display_name: 'Discord', color: { default: 'bg-[#5865F2]', hover: '#FF65F2' } },
|
||||
{ name: 'kanidm', display_name: 'Kanidm', color: { default: 'bg-red-500', hover: 'bg-red-700' } },
|
||||
{ name: 'google', display_name: 'Google' },
|
||||
]
|
||||
let first = true;
|
||||
for (const p of providers) {
|
||||
let b = document.createElement('button');
|
||||
if (first) b.classList.add('last:col-span-2');
|
||||
first = false;
|
||||
b.classList.add(...(
|
||||
'w-full text-white font-medium py-2 rounded-xl transition'
|
||||
.split(' ')
|
||||
));
|
||||
b.classList.add(`providerButton-${p.name}`)
|
||||
|
||||
const col = { default: p.color?.default ?? "bg-gray-600", hover: p.color?.hover ?? "bg-gray-700" };
|
||||
|
||||
for (const k of Object.keys(col)) {
|
||||
let c = (col as { [k: string]: string })[k].trim();
|
||||
if (c.startsWith('bg-')) {
|
||||
c = c.replace(/^bg-/, '');
|
||||
const customProp = c.match(/^\((.+)\)$/);
|
||||
const customVal = c.match(/^\[(.+)\]$/);
|
||||
|
||||
if (customProp)
|
||||
c = `var(${customProp[1]})`
|
||||
else if (customVal)
|
||||
c = customVal[1];
|
||||
else if (c === 'inherit')
|
||||
c = 'inherit';
|
||||
else if (c === 'current')
|
||||
c = 'currentColor';
|
||||
else if (c === 'transparent')
|
||||
c = 'transparent';
|
||||
else
|
||||
c = `var(--color-${c})`
|
||||
|
||||
}
|
||||
(col as { [k: string]: string })[k] = c;
|
||||
}
|
||||
|
||||
styleSheetElement.innerText += `.providerButton-${p.name} { background-color: ${col.default}; }\n`;
|
||||
styleSheetElement.innerText += `.providerButton-${p.name}:hover { background-color: ${col.hover}; }\n`;
|
||||
|
||||
b.dataset.display_name = p.display_name;
|
||||
b.dataset.name = p.name;
|
||||
if (p.icon_url) b.dataset.icon = p.icon_url;
|
||||
|
||||
b.innerHTML = `
|
||||
${p.icon_url ? `<img src="${p.icon_url}" alt="${p.display_name} Logo" />` : ''} <span class="">${p.display_name}</span>
|
||||
`
|
||||
b.addEventListener('click', () => {
|
||||
location.href = `/api/auth/oauth2/${p.name}/login`;
|
||||
})
|
||||
|
||||
dOtherLoginArea.insertAdjacentElement('afterbegin', b);
|
||||
}
|
||||
app?.appendChild(styleSheetElement);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
|
||||
addRoute('/login', handleLogin, { bypass_auth: true })
|
||||
15
frontend/src/pages/root/root.html
Normal file
15
frontend/src/pages/root/root.html
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
<div>
|
||||
Welcome a to <span> The Site de Boule </span>
|
||||
</div>
|
||||
<div>
|
||||
Welcome a to <span> The Site de Boule </span>
|
||||
</div>
|
||||
<div>
|
||||
Welcome a to <span> The Site de Boule </span>
|
||||
</div>
|
||||
<div>
|
||||
Welcome a to <span> The Site de Boule </span>
|
||||
</div>
|
||||
<div>
|
||||
Welcome a to <span> The Site de Boule </span>
|
||||
</div>
|
||||
14
frontend/src/pages/root/root.ts
Normal file
14
frontend/src/pages/root/root.ts
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import { addRoute, setTitle, type RouteHandlerParams } from "@app/routing";
|
||||
import page from './root.html?raw'
|
||||
|
||||
addRoute('/', (_: string) => {
|
||||
setTitle('ft boules')
|
||||
return page;
|
||||
})
|
||||
|
||||
|
||||
addRoute('/with_title/:title', (_: string, args: RouteHandlerParams) => {
|
||||
setTitle(args.title)
|
||||
console.log(`title should be '${args.title}'`);
|
||||
return page;
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue