Compare commits
9 Commits
d688101378
...
9472577d5e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9472577d5e | ||
|
|
f180f59546 | ||
|
|
0e12286f15 | ||
|
|
cf037db080 | ||
|
|
012a57956a | ||
|
|
6c18accae4 | ||
|
|
2b500ca187 | ||
|
|
afe0a0de54 | ||
|
|
3b08e49d6f |
@@ -1,55 +1,55 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants.js";
|
||||
import { refreshCookie, userDatesFromRFC3339 } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI } from '$lib/utils/constants.js';
|
||||
import { refreshCookie, userDatesFromRFC3339 } from '$lib/utils/helpers';
|
||||
|
||||
/** @type {import('@sveltejs/kit').Handle} */
|
||||
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 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);
|
||||
}
|
||||
|
||||
// get cookies from browser
|
||||
const jwt = event.cookies.get("jwt");
|
||||
const jwt = event.cookies.get('jwt');
|
||||
|
||||
if (!jwt) {
|
||||
// if there is no jwt load page as normal
|
||||
return await resolve(event);
|
||||
}
|
||||
const response = await fetch(`${BASE_API_URI}/backend/users/current`, {
|
||||
credentials: "include",
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`,
|
||||
},
|
||||
Cookie: `jwt=${jwt}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
event.cookies.delete("jwt", { path: "/" });
|
||||
event.cookies.delete('jwt', { path: '/' });
|
||||
return await resolve(event);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
refreshCookie(newToken, event);
|
||||
const newToken = response.headers.get('Set-Cookie');
|
||||
refreshCookie(newToken, event.cookies);
|
||||
|
||||
userDatesFromRFC3339(data.user);
|
||||
|
||||
const [subscriptionsResponse, licenceCategoriesResponse] = await Promise.all([
|
||||
fetch(`${BASE_API_URI}/backend/membership/subscriptions`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
}),
|
||||
fetch(`${BASE_API_URI}/backend/licence/categories`, {
|
||||
credentials: "include",
|
||||
headers: { Cookie: `jwt=${jwt}` },
|
||||
}),
|
||||
credentials: 'include',
|
||||
headers: { Cookie: `jwt=${jwt}` }
|
||||
})
|
||||
]);
|
||||
const [subscriptionsData, licence_categoriesData] = await Promise.all([
|
||||
subscriptionsResponse.json(),
|
||||
licenceCategoriesResponse.json(),
|
||||
licenceCategoriesResponse.json()
|
||||
]);
|
||||
event.locals.user = data.user;
|
||||
event.locals.subscriptions = subscriptionsData.subscriptions;
|
||||
|
||||
@@ -1,25 +1,51 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { onMount, onDestroy } from 'svelte';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
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 { writable } from 'svelte/store';
|
||||
|
||||
let isMobileMenuOpen = false;
|
||||
|
||||
/** @type{HTMLDivElement} */
|
||||
let headerContainer;
|
||||
|
||||
onMount(() => {
|
||||
console.log('Page data in Header:', $page);
|
||||
document.documentElement.setAttribute('data-theme', $theme);
|
||||
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
});
|
||||
|
||||
onDestroy(() => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
});
|
||||
$: {
|
||||
console.log('Page data updated:', $page);
|
||||
}
|
||||
// Create a theme store
|
||||
|
||||
const theme = writable(
|
||||
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() {
|
||||
theme.update((current) => {
|
||||
const newTheme = current === 'dark' ? 'bright' : 'dark';
|
||||
@@ -31,7 +57,7 @@
|
||||
</script>
|
||||
|
||||
<header class="header">
|
||||
<div class="header-container">
|
||||
<div class="header-container" bind:this={headerContainer}>
|
||||
<div class="header-left">
|
||||
<div class="header-crafted-by-container">
|
||||
<!-- <a href="https://tiny-bits.net/">
|
||||
@@ -42,7 +68,17 @@
|
||||
<!-- </a> -->
|
||||
</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 === '/'}>
|
||||
<a href="/">home</a>
|
||||
</div>
|
||||
@@ -188,4 +224,172 @@
|
||||
input:checked + .slider .fa-moon {
|
||||
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>
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
/** @type {boolean} */
|
||||
export let readonly = false;
|
||||
|
||||
/** @type {string} */
|
||||
export let backgroundColor = '--surface0'; // New prop for background color
|
||||
|
||||
/**
|
||||
* @param {Event} event - The input event
|
||||
*/
|
||||
@@ -110,7 +113,10 @@
|
||||
$: selectedColor = selectedOption ? selectedOption.color : '';
|
||||
</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'}
|
||||
<label class="form-control {readonly ? 'form-control--disabled' : ''}">
|
||||
<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 {
|
||||
transition:
|
||||
border-color 0.3s ease-in-out,
|
||||
|
||||
@@ -35,7 +35,8 @@ export default {
|
||||
issuing_country: 'Ausstellendes Land',
|
||||
subscription_name: 'Name des Tarifmodells',
|
||||
subscription_details: 'Beschreibe das Tarifmodell...',
|
||||
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...'
|
||||
subscription_conditions: 'Beschreibe die Bedingungen zur Nutzung...',
|
||||
search: 'Suchen...'
|
||||
},
|
||||
validation: {
|
||||
required: 'Eingabe benötigt',
|
||||
@@ -137,6 +138,7 @@ export default {
|
||||
actions: 'Aktionen',
|
||||
edit: 'Bearbeiten',
|
||||
delete: 'Löschen',
|
||||
search: 'Suche:',
|
||||
mandate_date_signed: 'Mandatserteilungsdatum',
|
||||
licence_categories: 'Führerscheinklassen',
|
||||
subscription_model: 'Mitgliedschatfsmodell',
|
||||
|
||||
@@ -184,13 +184,13 @@ export function formatError(obj) {
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
const match = newToken.match(/jwt=([^;]+)/);
|
||||
if (match) {
|
||||
event.cookies.set('jwt', match[1], {
|
||||
cookies.set('jwt', match[1], {
|
||||
path: '/',
|
||||
httpOnly: true,
|
||||
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,28 +1,28 @@
|
||||
import { BASE_API_URI } from "$lib/utils/constants";
|
||||
import { redirect } from "@sveltejs/kit";
|
||||
import { userDatesFromRFC3339, refreshCookie } from "$lib/utils/helpers";
|
||||
import { BASE_API_URI } from '$lib/utils/constants';
|
||||
import { redirect } from '@sveltejs/kit';
|
||||
import { userDatesFromRFC3339, refreshCookie } from '$lib/utils/helpers';
|
||||
|
||||
/** @type {import('./$types').LayoutServerLoad} */
|
||||
export async function load({ cookies, fetch, locals }) {
|
||||
const jwt = cookies.get("jwt");
|
||||
const jwt = cookies.get('jwt');
|
||||
try {
|
||||
// Fetch user data, subscriptions, and licence categories in parallel
|
||||
const response = await fetch(`${BASE_API_URI}/backend/users/all`, {
|
||||
credentials: "include",
|
||||
credentials: 'include',
|
||||
headers: {
|
||||
Cookie: `jwt=${jwt}`,
|
||||
},
|
||||
Cookie: `jwt=${jwt}`
|
||||
}
|
||||
});
|
||||
if (!response.ok) {
|
||||
// Clear the invalid JWT cookie
|
||||
cookies.delete("jwt", { path: "/" });
|
||||
throw redirect(302, "/auth/login?next=/");
|
||||
cookies.delete('jwt', { path: '/' });
|
||||
throw redirect(302, '/auth/login?next=/');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
// Check if the server sent a new token
|
||||
const newToken = response.headers.get("Set-Cookie");
|
||||
refreshCookie(newToken, null);
|
||||
const newToken = response.headers.get('Set-Cookie');
|
||||
refreshCookie(newToken, cookies);
|
||||
|
||||
/** @type {App.Locals['users']}*/
|
||||
const users = data.users;
|
||||
@@ -36,13 +36,13 @@ export async function load({ cookies, fetch, locals }) {
|
||||
subscriptions: locals.subscriptions,
|
||||
licence_categories: locals.licence_categories,
|
||||
users: locals.users,
|
||||
user: locals.user,
|
||||
user: locals.user
|
||||
};
|
||||
} catch (error) {
|
||||
console.error("Error fetching data:", error);
|
||||
console.error('Error fetching data:', error);
|
||||
// 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 UserEditForm from '$lib/components/UserEditForm.svelte';
|
||||
import SubscriptionEditForm from '$lib/components/SubscriptionEditForm.svelte';
|
||||
import InputField from '$lib/components/InputField.svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { page } from '$app/stores';
|
||||
import { applyAction, enhance } from '$app/forms';
|
||||
@@ -25,6 +26,59 @@
|
||||
let selectedSubscription = null;
|
||||
let showSubscriptionModal = 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.
|
||||
@@ -50,7 +104,7 @@
|
||||
selectedUser = null;
|
||||
selectedSubscription = null;
|
||||
if (form) {
|
||||
form.errors = undefined;
|
||||
form.errors = [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -117,13 +171,32 @@
|
||||
{#if activeSection === 'users'}
|
||||
<div class="section-header">
|
||||
<h2>{$t('users')}</h2>
|
||||
<div class="title-container">
|
||||
<InputField
|
||||
name="search"
|
||||
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 class="accordion">
|
||||
{#each users as user}
|
||||
{#each filteredUsers as user}
|
||||
<details class="accordion-item">
|
||||
<summary class="accordion-header">
|
||||
{user.first_name}
|
||||
@@ -352,6 +425,18 @@
|
||||
{/if}
|
||||
|
||||
<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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
@@ -369,7 +454,6 @@
|
||||
|
||||
.sidebar {
|
||||
width: 250px;
|
||||
min-height: 600px;
|
||||
background: var(--surface0);
|
||||
border-right: 1px solid var(--surface1);
|
||||
}
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
align-items: flex-end;
|
||||
width: 100%;
|
||||
max-width: 444px;
|
||||
margin-top: 30px;
|
||||
}
|
||||
|
||||
.forgot-password {
|
||||
|
||||
Reference in New Issue
Block a user