Compare commits
9 Commits
d688101378
...
9472577d5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9472577d5e | ||
|
|
f180f59546 | ||
|
|
0e12286f15 | ||
|
|
cf037db080 | ||
|
|
012a57956a | ||
|
|
6c18accae4 | ||
|
|
2b500ca187 | ||
|
|
afe0a0de54 | ||
|
|
3b08e49d6f |
@@ -1,60 +1,60 @@
|
|||||||
import { BASE_API_URI } from "$lib/utils/constants.js";
|
import { BASE_API_URI } from '$lib/utils/constants.js';
|
||||||
import { refreshCookie, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
import { refreshCookie, userDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||||
|
|
||||||
/** @type {import('@sveltejs/kit').Handle} */
|
/** @type {import('@sveltejs/kit').Handle} */
|
||||||
export async function handle({ event, resolve }) {
|
export async function handle({ event, resolve }) {
|
||||||
console.log("Hook started", event.url.pathname);
|
console.log('Hook started', event.url.pathname);
|
||||||
if (event.locals.user) {
|
if (event.locals.user) {
|
||||||
// if there is already a user in session load page as normal
|
// if there is already a user in session load page as normal
|
||||||
console.log("user is logged in");
|
console.log('user is logged in');
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
// get cookies from browser
|
// get cookies from browser
|
||||||
const jwt = event.cookies.get("jwt");
|
const jwt = event.cookies.get('jwt');
|
||||||
|
|
||||||
if (!jwt) {
|
if (!jwt) {
|
||||||
// if there is no jwt load page as normal
|
// if there is no jwt load page as normal
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
const response = await fetch(`${BASE_API_URI}/backend/users/current`, {
|
const response = await fetch(`${BASE_API_URI}/backend/users/current`, {
|
||||||
credentials: "include",
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `jwt=${jwt}`,
|
Cookie: `jwt=${jwt}`
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Clear the invalid JWT cookie
|
// Clear the invalid JWT cookie
|
||||||
event.cookies.delete("jwt", { path: "/" });
|
event.cookies.delete('jwt', { path: '/' });
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
|
|
||||||
// Check if the server sent a new token
|
// Check if the server sent a new token
|
||||||
const newToken = response.headers.get("Set-Cookie");
|
const newToken = response.headers.get('Set-Cookie');
|
||||||
refreshCookie(newToken, event);
|
refreshCookie(newToken, event.cookies);
|
||||||
|
|
||||||
userDatesFromRFC3339(data.user);
|
userDatesFromRFC3339(data.user);
|
||||||
|
|
||||||
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
||||||
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
||||||
credentials: "include",
|
credentials: 'include',
|
||||||
headers: { Cookie: `jwt=${jwt}` },
|
headers: { Cookie: `jwt=${jwt}` }
|
||||||
}),
|
}),
|
||||||
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
||||||
credentials: "include",
|
credentials: 'include',
|
||||||
headers: { Cookie: `jwt=${jwt}` },
|
headers: { Cookie: `jwt=${jwt}` }
|
||||||
}),
|
})
|
||||||
]);
|
]);
|
||||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||||
subscriptionsResponse.json(),
|
subscriptionsResponse.json(),
|
||||||
licenceCategoriesResponse.json(),
|
licenceCategoriesResponse.json()
|
||||||
]);
|
]);
|
||||||
event.locals.user = data.user;
|
event.locals.user = data.user;
|
||||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||||
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
event.locals.licence_categories = licence_categoriesData.licence_categories;
|
||||||
|
|
||||||
// load page as normal
|
// load page as normal
|
||||||
return await resolve(event);
|
return await resolve(event);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,51 @@
|
|||||||
<script>
|
<script>
|
||||||
import { onMount } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import { applyAction, enhance } from '$app/forms';
|
import { applyAction, enhance } from '$app/forms';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
// import Developer from "$lib/img/hero-image.png";
|
|
||||||
// import Avatar from "$lib/img/TeamAvatar.jpeg";
|
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
|
let isMobileMenuOpen = false;
|
||||||
|
|
||||||
|
/** @type{HTMLDivElement} */
|
||||||
|
let headerContainer;
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
console.log('Page data in Header:', $page);
|
console.log('Page data in Header:', $page);
|
||||||
document.documentElement.setAttribute('data-theme', $theme);
|
document.documentElement.setAttribute('data-theme', $theme);
|
||||||
|
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
});
|
||||||
$: {
|
$: {
|
||||||
console.log('Page data updated:', $page);
|
console.log('Page data updated:', $page);
|
||||||
}
|
}
|
||||||
// Create a theme store
|
|
||||||
const theme = writable(
|
const theme = writable(
|
||||||
typeof window !== 'undefined' ? localStorage.getItem('theme') || 'dark' : 'dark'
|
typeof window !== 'undefined' ? localStorage.getItem('theme') || 'dark' : 'dark'
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update theme and localStorage when changed
|
/**
|
||||||
|
* handle a click outside the menu to close it.
|
||||||
|
* @param {MouseEvent} event
|
||||||
|
*/
|
||||||
|
function handleClickOutside(event) {
|
||||||
|
if (
|
||||||
|
isMobileMenuOpen &&
|
||||||
|
event.target instanceof Node &&
|
||||||
|
!!headerContainer.contains(event.target)
|
||||||
|
) {
|
||||||
|
isMobileMenuOpen = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleMobileMenu() {
|
||||||
|
isMobileMenuOpen = !isMobileMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
function toggleTheme() {
|
function toggleTheme() {
|
||||||
theme.update((current) => {
|
theme.update((current) => {
|
||||||
const newTheme = current === 'dark' ? 'bright' : 'dark';
|
const newTheme = current === 'dark' ? 'bright' : 'dark';
|
||||||
@@ -31,7 +57,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<header class="header">
|
<header class="header">
|
||||||
<div class="header-container">
|
<div class="header-container" bind:this={headerContainer}>
|
||||||
<div class="header-left">
|
<div class="header-left">
|
||||||
<div class="header-crafted-by-container">
|
<div class="header-crafted-by-container">
|
||||||
<!-- <a href="https://tiny-bits.net/">
|
<!-- <a href="https://tiny-bits.net/">
|
||||||
@@ -42,7 +68,17 @@
|
|||||||
<!-- </a> -->
|
<!-- </a> -->
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-right">
|
<div class="mobile-menu-container">
|
||||||
|
<button
|
||||||
|
class="mobile-menu-toggle"
|
||||||
|
on:click={toggleMobileMenu}
|
||||||
|
aria-label="Toggle menu"
|
||||||
|
aria-expanded={isMobileMenuOpen}
|
||||||
|
>
|
||||||
|
<i class="fas {isMobileMenuOpen ? 'fa-times' : 'fa-bars'}"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="header-right" class:mobile-menu-open={isMobileMenuOpen}>
|
||||||
<div class="header-nav-item" class:active={$page.url.pathname === '/'}>
|
<div class="header-nav-item" class:active={$page.url.pathname === '/'}>
|
||||||
<a href="/">home</a>
|
<a href="/">home</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -188,4 +224,172 @@
|
|||||||
input:checked + .slider .fa-moon {
|
input:checked + .slider .fa-moon {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
.mobile-menu-toggle {
|
||||||
|
display: none;
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
.mobile-menu-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1;
|
||||||
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
|
padding: 3em 0 0;
|
||||||
|
background: var(--base);
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
width: 100%;
|
||||||
|
max-width: calc(1200px + 10em);
|
||||||
|
height: 5em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0 auto;
|
||||||
|
background-color: var(--base);
|
||||||
|
}
|
||||||
|
.header .header-container .header-left {
|
||||||
|
display: flex;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
.header .header-container .header-left .header-crafted-by-container {
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: space-between;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-left: 10px;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right .header-nav-item a,
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
transition: color 0.3s ease-in-out;
|
||||||
|
display: block;
|
||||||
|
padding: 20px 0;
|
||||||
|
border: none;
|
||||||
|
color: var(--subtext0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right .header-nav-item:hover a,
|
||||||
|
.header .header-container .header-right .header-nav-item:hover button {
|
||||||
|
color: var(--lavender);
|
||||||
|
}
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
.header {
|
||||||
|
padding: 3em 5rem 0;
|
||||||
|
}
|
||||||
|
.header .header-container {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
margin-left: 26px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.mobile-menu-container {
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-menu-toggle {
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container {
|
||||||
|
z-index: 1;
|
||||||
|
position: relative;
|
||||||
|
padding: 0 1rem;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
height: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right {
|
||||||
|
display: none;
|
||||||
|
top: 4rem;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
background: var(--base);
|
||||||
|
padding: 1rem;
|
||||||
|
border-bottom: 1px solid var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-right.mobile-menu-open {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0.5rem 0;
|
||||||
|
border-bottom: 1px solid var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-nav-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right .header-nav-item {
|
||||||
|
margin: 0;
|
||||||
|
text-align: center;
|
||||||
|
border-top: 1px solid var(--surface1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.header .header-container .header-right .header-nav-item a,
|
||||||
|
.header .header-container .header-right .header-nav-item button {
|
||||||
|
padding: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -37,6 +37,9 @@
|
|||||||
/** @type {boolean} */
|
/** @type {boolean} */
|
||||||
export let readonly = false;
|
export let readonly = false;
|
||||||
|
|
||||||
|
/** @type {string} */
|
||||||
|
export let backgroundColor = '--surface0'; // New prop for background color
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Event} event - The input event
|
* @param {Event} event - The input event
|
||||||
*/
|
*/
|
||||||
@@ -110,7 +113,10 @@
|
|||||||
$: selectedColor = selectedOption ? selectedOption.color : '';
|
$: selectedColor = selectedOption ? selectedOption.color : '';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}">
|
<div
|
||||||
|
class="input-box {type === 'checkbox' ? 'checkbox-container' : ''}"
|
||||||
|
style="background-color: var({backgroundColor});"
|
||||||
|
>
|
||||||
{#if type === 'checkbox'}
|
{#if type === 'checkbox'}
|
||||||
<label class="form-control {readonly ? 'form-control--disabled' : ''}">
|
<label class="form-control {readonly ? 'form-control--disabled' : ''}">
|
||||||
<input
|
<input
|
||||||
|
|||||||
104
frontend/src/lib/css/styles.min.css
vendored
104
frontend/src/lib/css/styles.min.css
vendored
@@ -204,110 +204,6 @@ li strong {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: 1;
|
|
||||||
box-sizing: border-box;
|
|
||||||
width: 100%;
|
|
||||||
padding: 3em 0 0;
|
|
||||||
background: var(--base);
|
|
||||||
}
|
|
||||||
.header.top-banner-open {
|
|
||||||
margin-top: 5px;
|
|
||||||
transition: all 0.2s linear;
|
|
||||||
}
|
|
||||||
.header .header-container {
|
|
||||||
width: 100%;
|
|
||||||
max-width: calc(1200px + 10em);
|
|
||||||
height: 5em;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
margin: 0 auto;
|
|
||||||
background-color: var(--base);
|
|
||||||
}
|
|
||||||
.header .header-container .header-left {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
}
|
|
||||||
.header .header-container .header-left .header-crafted-by-container {
|
|
||||||
font-size: 18px;
|
|
||||||
font-weight: 300;
|
|
||||||
}
|
|
||||||
.header .header-container .header-left .header-crafted-by-container a {
|
|
||||||
display: flex;
|
|
||||||
color: var(--subtext0);
|
|
||||||
border: none;
|
|
||||||
}
|
|
||||||
.header .header-container .header-left .header-crafted-by-container a img {
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
.header .header-container .header-left .header-crafted-by-container a span {
|
|
||||||
display: inline-block;
|
|
||||||
margin: 2px 1ch 0 0;
|
|
||||||
}
|
|
||||||
.header .header-container .header-left .header-crafted-by-container .auth0 {
|
|
||||||
margin-left: 1ch;
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right {
|
|
||||||
display: flex;
|
|
||||||
flex-grow: 1;
|
|
||||||
justify-content: space-between;
|
|
||||||
letter-spacing: 1px;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right .header-nav-item {
|
|
||||||
text-transform: uppercase;
|
|
||||||
margin-left: 10px;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right .header-nav-item button {
|
|
||||||
all: unset;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right .header-nav-item.active a,
|
|
||||||
.header .header-container .header-right .header-nav-item.active button {
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .header-container .header-right a img {
|
|
||||||
margin-top: -0.4rem;
|
|
||||||
height: 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .header-container .header-right .header-nav-item a,
|
|
||||||
.header .header-container .header-right .header-nav-item button {
|
|
||||||
transition: color 0.3s ease-in-out;
|
|
||||||
display: block;
|
|
||||||
padding: 20px 0;
|
|
||||||
border: none;
|
|
||||||
color: var(--subtext0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header .header-container .header-right .header-nav-item:hover a,
|
|
||||||
.header .header-container .header-right .header-nav-item:hover button {
|
|
||||||
color: var(--lavender);
|
|
||||||
}
|
|
||||||
@media (min-width: 680px) {
|
|
||||||
.header {
|
|
||||||
padding: 3em 5rem 0;
|
|
||||||
}
|
|
||||||
.header.top-banner-open {
|
|
||||||
margin-top: 48px;
|
|
||||||
}
|
|
||||||
.header .header-container {
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right {
|
|
||||||
justify-content: flex-end;
|
|
||||||
}
|
|
||||||
.header .header-container .header-right .header-nav-item {
|
|
||||||
margin-left: 26px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.button-dark {
|
.button-dark {
|
||||||
transition:
|
transition:
|
||||||
border-color 0.3s ease-in-out,
|
border-color 0.3s ease-in-out,
|
||||||
|
|||||||
@@ -35,7 +35,8 @@ export default {
|
|||||||
issuing_country: 'Ausstellendes Land',
|
issuing_country: 'Ausstellendes Land',
|
||||||
subscription_name: 'Name des Tarifmodells',
|
subscription_name: 'Name des Tarifmodells',
|
||||||
subscription_details: 'Beschreibe das Tarifmodell...',
|
subscription_details: 'Beschreibe das Tarifmodell...',
|
||||||
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...'
|
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...',
|
||||||
|
search: 'Suchen...'
|
||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
required: 'Eingabe benötigt',
|
required: 'Eingabe benötigt',
|
||||||
@@ -137,6 +138,7 @@ export default {
|
|||||||
actions: 'Aktionen',
|
actions: 'Aktionen',
|
||||||
edit: 'Bearbeiten',
|
edit: 'Bearbeiten',
|
||||||
delete: 'Löschen',
|
delete: 'Löschen',
|
||||||
|
search: 'Suche:',
|
||||||
mandate_date_signed: 'Mandatserteilungsdatum',
|
mandate_date_signed: 'Mandatserteilungsdatum',
|
||||||
licence_categories: 'Führerscheinklassen',
|
licence_categories: 'Führerscheinklassen',
|
||||||
subscription_model: 'Mitgliedschatfsmodell',
|
subscription_model: 'Mitgliedschatfsmodell',
|
||||||
|
|||||||
@@ -184,13 +184,13 @@ export function formatError(obj) {
|
|||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {string | null} newToken - The new token for the cookie to set
|
* @param {string | null} newToken - The new token for the cookie to set
|
||||||
* @param {import('@sveltejs/kit').RequestEvent } event - The event object
|
* @param {import('@sveltejs/kit').Cookies } cookies - The event object
|
||||||
*/
|
*/
|
||||||
export function refreshCookie(newToken, event) {
|
export function refreshCookie(newToken, cookies) {
|
||||||
if (newToken) {
|
if (newToken) {
|
||||||
const match = newToken.match(/jwt=([^;]+)/);
|
const match = newToken.match(/jwt=([^;]+)/);
|
||||||
if (match) {
|
if (match) {
|
||||||
event.cookies.set('jwt', match[1], {
|
cookies.set('jwt', match[1], {
|
||||||
path: '/',
|
path: '/',
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
secure: process.env.NODE_ENV === 'production', // Secure in production
|
secure: process.env.NODE_ENV === 'production', // Secure in production
|
||||||
@@ -200,36 +200,3 @@ export function refreshCookie(newToken, event) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a debounced version of an input event handler.
|
|
||||||
*
|
|
||||||
* @param {HTMLElement} element - The HTML element to attach the debounced event to.
|
|
||||||
* @param {number} duration - The delay in milliseconds before the event is triggered after the last input.
|
|
||||||
* @returns {Object} - An object with a `destroy` method to clean up the event listener.
|
|
||||||
*
|
|
||||||
* @example
|
|
||||||
* <input use:debounce={300} on:debouncedinput={handleInput} />
|
|
||||||
*/
|
|
||||||
export function debounce(element, duration) {
|
|
||||||
/** @type{NodeJS.Timeout} */
|
|
||||||
let timer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param {CustomEventInit} e
|
|
||||||
*/
|
|
||||||
function input(e) {
|
|
||||||
clearTimeout(timer);
|
|
||||||
timer = setTimeout(() => {
|
|
||||||
element.dispatchEvent(new CustomEvent('debouncedinput', e));
|
|
||||||
}, duration);
|
|
||||||
}
|
|
||||||
|
|
||||||
element.addEventListener('input', input);
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
element.removeEventListener('input', input);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,48 +1,48 @@
|
|||||||
import { BASE_API_URI } from "$lib/utils/constants";
|
import { BASE_API_URI } from '$lib/utils/constants';
|
||||||
import { redirect } from "@sveltejs/kit";
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { userDatesFromRFC3339, refreshCookie } from "$lib/utils/helpers";
|
import { userDatesFromRFC3339, refreshCookie } from '$lib/utils/helpers';
|
||||||
|
|
||||||
/** @type {import('./$types').LayoutServerLoad} */
|
/** @type {import('./$types').LayoutServerLoad} */
|
||||||
export async function load({ cookies, fetch, locals }) {
|
export async function load({ cookies, fetch, locals }) {
|
||||||
const jwt = cookies.get("jwt");
|
const jwt = cookies.get('jwt');
|
||||||
try {
|
try {
|
||||||
// Fetch user data, subscriptions, and licence categories in parallel
|
// Fetch user data, subscriptions, and licence categories in parallel
|
||||||
const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
|
const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
|
||||||
credentials: "include",
|
credentials: 'include',
|
||||||
headers: {
|
headers: {
|
||||||
Cookie: `jwt=${jwt}`,
|
Cookie: `jwt=${jwt}`
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
// Clear the invalid JWT cookie
|
// Clear the invalid JWT cookie
|
||||||
cookies.delete("jwt", { path: "/" });
|
cookies.delete('jwt', { path: '/' });
|
||||||
throw redirect(302, "/auth/login?next=/");
|
throw redirect(302, '/auth/login?next=/');
|
||||||
}
|
}
|
||||||
|
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
// Check if the server sent a new token
|
// Check if the server sent a new token
|
||||||
const newToken = response.headers.get("Set-Cookie");
|
const newToken = response.headers.get('Set-Cookie');
|
||||||
refreshCookie(newToken, null);
|
refreshCookie(newToken, cookies);
|
||||||
|
|
||||||
/** @type {App.Locals['users']}*/
|
/** @type {App.Locals['users']}*/
|
||||||
const users = data.users;
|
const users = data.users;
|
||||||
|
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
userDatesFromRFC3339(user);
|
userDatesFromRFC3339(user);
|
||||||
});
|
});
|
||||||
|
|
||||||
locals.users = users;
|
locals.users = users;
|
||||||
return {
|
return {
|
||||||
subscriptions: locals.subscriptions,
|
subscriptions: locals.subscriptions,
|
||||||
licence_categories: locals.licence_categories,
|
licence_categories: locals.licence_categories,
|
||||||
users: locals.users,
|
users: locals.users,
|
||||||
user: locals.user,
|
user: locals.user
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching data:", error);
|
console.error('Error fetching data:', error);
|
||||||
// In case of any error, clear the JWT cookie
|
// In case of any error, clear the JWT cookie
|
||||||
cookies.delete("jwt", { path: "/" });
|
cookies.delete('jwt', { path: '/' });
|
||||||
|
|
||||||
throw redirect(302, "/auth/login?next=/");
|
throw redirect(302, '/auth/login?next=/');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import Modal from '$lib/components/Modal.svelte';
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
import UserEditForm from '$lib/components/UserEditForm.svelte';
|
import UserEditForm from '$lib/components/UserEditForm.svelte';
|
||||||
import SubscriptionEditForm from '$lib/components/SubscriptionEditForm.svelte';
|
import SubscriptionEditForm from '$lib/components/SubscriptionEditForm.svelte';
|
||||||
|
import InputField from '$lib/components/InputField.svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { applyAction, enhance } from '$app/forms';
|
import { applyAction, enhance } from '$app/forms';
|
||||||
@@ -25,6 +26,59 @@
|
|||||||
let selectedSubscription = null;
|
let selectedSubscription = null;
|
||||||
let showSubscriptionModal = false;
|
let showSubscriptionModal = false;
|
||||||
let showUserModal = false;
|
let showUserModal = false;
|
||||||
|
let searchTerm = '';
|
||||||
|
|
||||||
|
$: filteredUsers = searchTerm ? getFilteredUsers() : users;
|
||||||
|
|
||||||
|
function handleMailButtonClick() {
|
||||||
|
const subject = 'Important Announcement';
|
||||||
|
const body = `Hello everyone,\n\nThis is an important message.`;
|
||||||
|
const bccEmails = filteredUsers
|
||||||
|
.map((/** @type{App.Locals['user']}*/ user) => user.email)
|
||||||
|
.join(',');
|
||||||
|
const encodedSubject = encodeURIComponent(subject);
|
||||||
|
const encodedBody = encodeURIComponent(body);
|
||||||
|
const mailtoLink = `mailto:?bcc=${bccEmails}&subject=${encodedSubject}&body=${encodedBody}`;
|
||||||
|
window.location.href = mailtoLink; // Open the mail client
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* returns a set of users depending on the entered search query
|
||||||
|
* @return {App.Locals['user'][]}*/
|
||||||
|
const getFilteredUsers = () => {
|
||||||
|
if (!searchTerm.trim()) return users;
|
||||||
|
|
||||||
|
const term = searchTerm.trim().toLowerCase();
|
||||||
|
|
||||||
|
return users.filter((/** @type{App.Locals['user']}*/ user) => {
|
||||||
|
const basicMatch = [
|
||||||
|
user.first_name?.toLowerCase(),
|
||||||
|
user.last_name?.toLowerCase(),
|
||||||
|
user.email?.toLowerCase(),
|
||||||
|
user.address?.toLowerCase(),
|
||||||
|
user.city?.toLowerCase(),
|
||||||
|
user.dateofbirth?.toLowerCase(),
|
||||||
|
user.phone?.toLowerCase(),
|
||||||
|
user.company?.toLowerCase(),
|
||||||
|
user.licence?.number?.toLowerCase()
|
||||||
|
].some((field) => field?.includes(term));
|
||||||
|
|
||||||
|
const subscriptionMatch = user.membership?.subscription_model?.name
|
||||||
|
?.toLowerCase()
|
||||||
|
.includes(term);
|
||||||
|
|
||||||
|
const licenceCategoryMatch = user.licence?.categories?.some((cat) =>
|
||||||
|
cat.category.toLowerCase().includes(term)
|
||||||
|
);
|
||||||
|
|
||||||
|
const addressMatch = [
|
||||||
|
user.address?.toLowerCase(),
|
||||||
|
user.zip_code?.toLowerCase(),
|
||||||
|
user.city?.toLowerCase()
|
||||||
|
].some((field) => field?.includes(term));
|
||||||
|
return basicMatch || subscriptionMatch || licenceCategoryMatch || addressMatch;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens the edit modal for the selected user.
|
* Opens the edit modal for the selected user.
|
||||||
@@ -50,7 +104,7 @@
|
|||||||
selectedUser = null;
|
selectedUser = null;
|
||||||
selectedSubscription = null;
|
selectedSubscription = null;
|
||||||
if (form) {
|
if (form) {
|
||||||
form.errors = undefined;
|
form.errors = [];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -117,13 +171,32 @@
|
|||||||
{#if activeSection === 'users'}
|
{#if activeSection === 'users'}
|
||||||
<div class="section-header">
|
<div class="section-header">
|
||||||
<h2>{$t('users')}</h2>
|
<h2>{$t('users')}</h2>
|
||||||
<button class="btn primary" on:click={() => openEditUserModal(null)}>
|
<div class="title-container">
|
||||||
<i class="fas fa-plus"></i>
|
<InputField
|
||||||
{$t('add_new')}
|
name="search"
|
||||||
</button>
|
placeholder={$t('placeholder.search')}
|
||||||
|
backgroundColor="--base"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- <input type="text" bind:value={searchTerm} placeholder={$t('placeholder.search')} /> -->
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
class="btn primary"
|
||||||
|
aria-label="Mail Users"
|
||||||
|
on:click={() => handleMailButtonClick()}
|
||||||
|
>
|
||||||
|
<i class="fas fa-envelope"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button class="btn primary" on:click={() => openEditUserModal(null)}>
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{$t('add_new')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="accordion">
|
<div class="accordion">
|
||||||
{#each users as user}
|
{#each filteredUsers as user}
|
||||||
<details class="accordion-item">
|
<details class="accordion-item">
|
||||||
<summary class="accordion-header">
|
<summary class="accordion-header">
|
||||||
{user.first_name}
|
{user.first_name}
|
||||||
@@ -352,6 +425,18 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
.title-container {
|
||||||
|
margin: 0 1rem;
|
||||||
|
flex-grow: 1;
|
||||||
|
max-width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
.container {
|
.container {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -369,7 +454,6 @@
|
|||||||
|
|
||||||
.sidebar {
|
.sidebar {
|
||||||
width: 250px;
|
width: 250px;
|
||||||
min-height: 600px;
|
|
||||||
background: var(--surface0);
|
background: var(--surface0);
|
||||||
border-right: 1px solid var(--surface1);
|
border-right: 1px solid var(--surface1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 444px;
|
max-width: 444px;
|
||||||
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.forgot-password {
|
.forgot-password {
|
||||||
|
|||||||
Reference in New Issue
Block a user