Refactor: Organize src/lib into subdirectories and move files accordingly.

This commit is contained in:
Scott Idem
2025-11-13 16:09:03 -05:00
parent 6d8089bdbd
commit 94832c2471
36 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,27 @@
<script lang="ts">
export let log_lvl: number = 0;
export let site_google_tracking_id: string = '';
if (log_lvl) {
console.log(`AE Analytics: site_google_tracking_id = `, site_google_tracking_id);
}
if (typeof window !== 'undefined') {
window.dataLayer = window.dataLayer || [];
window.gtag = function gtag(): void {
window.dataLayer.push(arguments);
};
window.gtag('js', new Date());
window.gtag('config', site_google_tracking_id);
if (log_lvl) {
console.log(`AE Analytics: Google Analytics Tracking ID = `, site_google_tracking_id);
}
}
</script>
<svelte:head>
<script
async
src="https://www.googletagmanager.com/gtag/js?id={site_google_tracking_id}">
</script>
</svelte:head>

View File

@@ -0,0 +1,620 @@
<script lang="ts">
// *** Import Svelte specific
import { onDestroy, onMount, tick } from 'svelte';
import { afterNavigate } from '$app/navigation';
// *** Import other supporting libraries
// import { liveQuery } from "dexie";
import {
ShieldEllipsis, ShieldMinus, ShieldPlus, ShieldUser,
User, UserCheck
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
// import { core_func } from '$lib/ae_core/ae_core_functions';
// Ideally the Event related stores should not be imported here?
import { events_loc } from '$lib/ae_events_stores';
// import { db_events } from "$lib/db_events";
// export let hidden: boolean = false;
// *** Setup Svelte properties
interface Props {
log_lvl?: number;
hide?: null|boolean;
focus_input: boolean;
expand?: boolean;
show_passcode_input: boolean;
trigger_clear_access: null|boolean;
}
let {
log_lvl = $bindable(0),
hide = $bindable(false),
focus_input = $bindable(false),
expand = $bindable(false),
show_passcode_input = $bindable(false),
trigger_clear_access = $bindable(null),
}: Props = $props();
let entered_passcode: null|string = $state(null);
let checked_passcode: null|string = $state(null);
// let password_checked: boolean = $state(false);
// let entered_passcode: null|string = '';
// let show_passcode_input: boolean = $state(false);
// let show_passcode_input: boolean = false;
// let trigger: null|string|boolean = null;
let trigger: null|string|boolean = $state(null);
// const dispatch = createEventDispatcher();
// WARNING: There is a bug (I think) around here related to the entered_passcode not being cleared. There seems to be something different about how Svelte handles state in this component compared to the others. This might be related to the `$effect` or `$derived` usage. Maybe there are conflicting things trying to update the $ae_loc store at the same time.
onMount(() => {
log_lvl = 2;
if (log_lvl > 1) {
console.log('** Element Mounted: ** Element Access Type');
}
entered_passcode = '';
trigger = null;
// /** @type {HTMLElement | null} */
// const to_focus = document.getElementById('access_passcode_input');
// to_focus?.focus();
});
onDestroy(() => {
if (log_lvl > 1) {
console.log('** Element Destroyed: ** Element Access Type');
}
// Clean up any references or listeners if needed
entered_passcode = null; // Clear the entered passcode
show_passcode_input = false;
// Reset trigger
trigger = null;
});
// afterNavigate(() => {
// /** @type {HTMLElement | null} */
// const to_focus = document.getElementById('access_passcode_input');
// to_focus?.focus();
// });
$effect(() => {
if (entered_passcode && entered_passcode.length >= 5 && entered_passcode != checked_passcode) {
checked_passcode = entered_passcode;
if (log_lvl) {
console.log(`entered_passcode=${entered_passcode}`);
}
handle_check_access_type_passcode();
}
});
$effect(() => {
if (trigger && $ae_loc.access_type) {
trigger = false;
if (log_lvl) {
console.log(`$ae_loc.access_type=${$ae_loc.access_type}`);
}
let access_checks_results = ae_util.process_permission_checks($ae_loc.access_type);
$ae_loc = {...$ae_loc, ...access_checks_results};
$ae_loc = $ae_loc;
$ae_loc.sys_menu.expand = false;
} else if (trigger) {
trigger = false;
if (log_lvl) {
console.log(`$ae_loc.access_type=not set`);
}
// Send an empty string to reset the permissions. This is the same as sending 'anonymous'.
let access_checks_results = ae_util.process_permission_checks('');
$ae_loc = {...$ae_loc, ...access_checks_results};
$ae_loc = $ae_loc;
}
});
// This does not seem to work. I feel like it should though...?
$effect(() => {
if (!hide && focus_input) {
focus_input = false;
log_lvl = 2;
// await tick();
// document.getElementById('access_passcode_input')?.focus();
if (log_lvl > 1) {
console.log('Effect: Setting focus on the passcode input field');
}
/** @type {HTMLElement | null} */
const to_focus = document.getElementById('access_passcode_input');
to_focus?.focus();
}
});
$effect(async () => {
if (trigger_clear_access) {
trigger_clear_access = false;
if (log_lvl) {
console.log(`trigger_clear_access=${trigger_clear_access}`);
}
handle_clear_access();
}
});
function handle_check_access_type_passcode() {
if (log_lvl > 1) {
console.log(`*** handle_check_access_type_passcode() *** passcode list:`, $ae_loc.site_access_code_kv);
}
// Reminder: super > manager > administrator > trusted > public > authenticated > anonymous
if (entered_passcode && entered_passcode.length >= 5) {
if ($ae_loc.site_access_code_kv.super.length >= 8 && $ae_loc.site_access_code_kv.super == entered_passcode) {
console.log('Super passcode matched');
window.localStorage.setItem('access_type', 'super');
$ae_loc.access_type = 'super';
} else if ($ae_loc.site_access_code_kv.manager.length >= 5 && $ae_loc.site_access_code_kv.manager == entered_passcode) {
console.log('Manager passcode matched');
window.localStorage.setItem('access_type', 'manager');
$ae_loc.access_type = 'manager';
} else if ($ae_loc.site_access_code_kv.administrator.length >= 5 && $ae_loc.site_access_code_kv.administrator == entered_passcode) {
console.log('Administrator passcode matched');
window.localStorage.setItem('access_type', 'administrator');
$ae_loc.access_type = 'administrator';
} else if ($ae_loc.site_access_code_kv.trusted.length >= 5 && $ae_loc.site_access_code_kv.trusted == entered_passcode) {
console.log('Trusted passcode matched');
window.localStorage.setItem('access_type', 'trusted');
$ae_loc.access_type = 'trusted';
} else if ($ae_loc.site_access_code_kv.public.length >= 5 && $ae_loc.site_access_code_kv.public == entered_passcode) {
console.log('Public passcode matched');
window.localStorage.setItem('access_type', 'public');
$ae_loc.access_type = 'public';
} else if ($ae_loc.site_access_code_kv.authenticated == entered_passcode) {
console.log('Authenticated passcode matched');
window.localStorage.setItem('access_type', 'authenticated');
$ae_loc.access_type = 'authenticated';
} else {
if (log_lvl > 1) {
console.log('Entered passcode does not match any of the site access codes.');
}
if ($ae_loc.access_type != 'anonymous') {
console.log('Access type is not anonymous');
}
// window.localStorage.setItem('access_type', 'anonymous');
// $ae_loc.access_type = 'anonymous';
// trigger = 'process_permission_check';
// $ae_loc = $ae_loc; // Trigger Svelte just in case
// ae_loc.set($ae_loc);
// console.log($ae_loc);
// dispatch_access_type_changed();
return false;
}
entered_passcode = '';
trigger = 'process_permission_check';
$ae_loc.app_cfg.show_element__menu = false;
$ae_loc.app_cfg.show_element__menu_btn = true;
// WARNING 2024-08-21: For some reason the config element does not auto show or hide when the access type changes.
if (!$ae_loc.iframe && $ae_loc.authenticated_access) {
$ae_loc.app_cfg.show_element__access_type = true;
$ae_loc.app_cfg.show_element__cfg = true;
} else if ($ae_loc.iframe && $ae_loc.trusted_access) {
$ae_loc.app_cfg.show_element__access_type = true;
$ae_loc.app_cfg.show_element__cfg = true;
} else {
$ae_loc.app_cfg.show_element__access_type = true;
$ae_loc.app_cfg.show_element__cfg = false;
}
// dispatch_access_type_changed();
return true;
} else {
if (log_lvl > 1) {
console.log('Entered passcode too short.');
}
// $ae_loc.access_type = null; // 'anonymous';
// dispatch_access_type_changed()
return null;
}
}
function handle_clear_access() {
// console.log('handle_clear_access()');
// NOTE: I think it makes since to reset this to anonymous even if logged in as an admin or similar.
window.localStorage.setItem('access_type', 'anonymous');
// $ae_loc.access_type = null; // 'anonymous';
// Revert back to the user's access type after quick access (temporarily escalate permissions) is turned off.
$ae_loc.access_type = $ae_loc.user_access_type ?? 'anonymous';
trigger = 'process_permission_check';
entered_passcode = ''; // Clear the entered passcode
show_passcode_input = true;
$ae_loc.app_cfg.show_element__menu = false;
$ae_loc.app_cfg.show_element__menu_btn = true;
$ae_loc.edit_mode = false;
return true;
}
</script>
<section
id="AE-Quick-Access-Type"
class="
ae_access_type
hidden-print
bg-blue-100 text-gray-900
dark:bg-blue-800 dark:text-gray-200
flex flex-col flex-wrap gap-1
items-end justify-center
w-full
p-1
border-2 border-gray-200
duration-300 delay-150 hover:delay-1000 hover:ease-out
transition-all
"
class:hidden={hide}
>
<!-- class:hidden={!$ae_sess.show__sign_in_out__fields} -->
<header
class="ae_header hidden"
>
<h2 class="text-sm text-center font-semibold">
Passcode Sign In
</h2>
</header>
<!-- Show list of authorized sessions and presentations for a user -->
<!-- {#if $ae_loc.access_type == 'administrator'}
{#if $events_loc.auth__kv.session}
Sessions:
<ul>
{#each Object.entries($events_loc.auth__kv.session) as [key, value]}
<li><a href="/events/{event_id}/session/{key}">
{key}
</a></li>
{/each}
</ul>
{/if}
{/if} -->
<div class="transition-all">
{#if $ae_loc.trusted_access && $ae_loc.edit_mode}
{#if $ae_loc.manager_access}
{#if $ae_loc?.sync_local_config}
<button
type="button"
onclick={() => {
$ae_loc.sync_local_config = false;
$events_loc.pres_mgmt.sync_local_config = false;
$ae_loc.lock_config = false;
$events_loc.pres_mgmt.lock_config = false;
// dispatch_sync_local_config_changed();
// tick();
return false;
}}
class="btn btn-sm preset-tonal-success border border-success-500 hover:preset-filled-success-500 transition-all hover:transition-all *:hover:inline"
title="Syncing the local configuration with the remote configuration."
>
<span class="fas fa-sync m-1"></span>
<span class="hidden">
Sync
</span>
</button>
{:else}
<button
type="button"
onclick={() => {
$ae_loc.sync_local_config = true;
$events_loc.pres_mgmt.sync_local_config = true;
$ae_loc.lock_config = true;
$events_loc.pres_mgmt.lock_config = true;
// dispatch_sync_local_config_changed();
// tick();
return true;
}}
class="btn btn-sm preset-tonal-warning border border-warning-500 hover:preset-filled-warning-500 transition-all hover:transition-all *:hover:inline"
title="Currently not syncing with the remote server. Re-sync the local configuration with the remote configuration?"
>
<span class="fas fa-unlink m-1"></span>
<span class="hidden">
Re-sync?
</span>
</button>
{/if}
{/if}
<!-- {#if $ae_loc.edit_mode}
<button
type="button"
onclick={() => {
$ae_loc.edit_mode = false;
// dispatch_edit_mode_changed();
}}
class="btn btn-sm px-2 variant-ghost-success hover:variant-filled-success transition-all"
title="Edit mode is currently enabled. Click to disable."
>
<span class="fas fa-toggle-on mx-1"></span>
Edit Mode On
</button>
{:else}
<button
type="button"
onclick={() => {
$ae_loc.edit_mode = true;
// dispatch_edit_mode_changed();
}}
class="btn btn-sm px-2 variant-ghost-warning hover:variant-filled-warning transition-all space-x-1 *:hover:inline"
title="Edit mode is currently disabled. Click to enable."
>
<span class="fas fa-toggle-off mx-1"></span>
Edit
<span class="hidden">
Mode?
</span>
</button>
{/if} -->
{/if}
</div>
<div class="flex flex-row flex-wrap gap-1 items-end justify-end w-full transition-all">
{#if $ae_loc?.access_type && $ae_loc?.access_type == 'anonymous' && 1==3}
<span>
<button
type="button"
onclick={() => {
// handle_check_access_type_passcode();
trigger = true;
}}
class="btn btn-sm preset-tonal-success hover:preset-filled-warning-500 access_type_unlock_btn transition-all"
title="Anonymous public access is currently set. Access mode is disabled/locked."
>
<span class="fas fa-lock mx-1"></span>
<span class="lock_icon">Locked</span>
<span class="fas fa-unlock mx-1 unlock_icon hidden"></span>
{#if (show_passcode_input)}
<span class="unlock_text">Cancel</span>
{:else}
<span class="unlock_text">Access?</span>
{/if}
</button>
</span>
{/if}
{#if ($ae_loc?.access_type && $ae_loc?.access_type != 'anonymous')}
<span class="flex flex-row gap-1 items-center justify-center">
<!-- <span class="fas fa-unlock mx-1"></span> -->
<ShieldPlus class="inline-block" />
<span
class="*:hover:inline"
title={`Current access type/level: ${$ae_loc.access_type}`}
>
{#if $ae_loc.access_type == 'super'}
<span class="fas fa-hat-wizard m-1"></span>
<span class="hidden">Super</span>
{:else if $ae_loc.access_type == 'manager'}
<span class="fas fa-user-shield m-1"></span>
<span class="hidden">Manager</span>
{:else if $ae_loc.access_type == 'administrator'}
<span class="fas fa-user-ninja m-1"></span>
<span class="hidden">Administrator</span>
{:else if $ae_loc.access_type == 'trusted'}
<span class="fas fa-user-check m-1"></span>
<span class="hidden">Trusted Access</span>
{:else if $ae_loc.access_type == 'public'}
Public
<span class="hidden">Access</span>
{:else if $ae_loc.access_type == 'authenticated'}
Authenticated
<span class="hidden">Access</span>
{:else if $ae_loc.access_type == 'anonymous'}
Anonymous Access
{:else}
Unknown Access
{/if}
</span>
{#if $ae_loc?.user_access_type && $ae_loc?.access_type == $ae_loc?.user_access_type && !show_passcode_input}
<button
type="button"
onclick={() => {
// handle_clear_access();
// trigger_clear_access = true;
show_passcode_input = true;
}}
class="btn btn-sm variant-outline-surface hover:preset-tonal-warning border border-warning-500 transition-all"
title={`Current user access level: "${$ae_loc.user_access_type}". Click use passcode for a different access level.`}
>
<!-- <span class="fas fa-lock mx-1"></span> -->
<!-- <ShieldMinus /> -->
<ShieldEllipsis class="inline-block" />
Passcode?
</button>
{:else if (!show_passcode_input)}
<button
type="button"
onclick={() => {
// handle_clear_access();
trigger_clear_access = true;
show_passcode_input = true;
// show_passcode_input = true;
}}
class="btn btn-sm variant-outline-warning hover:preset-tonal-warning border border-warning-500 transition-all"
title={`Current access level: "${$ae_loc.access_type}". Click to clear the temporary access level.`}
>
<!-- <span class="fas fa-lock mx-1"></span> -->
<ShieldMinus class="inline-block" />
Clear?
</button>
{/if}
</span>
{/if}
{#if (show_passcode_input)}
<span class="flex flex-row gap-1 items-center justify-between w-full">
<span>
<ShieldEllipsis class="inline-block text-gray-500" />
<span class="unlock_text text-sm">Passcode:</span>
</span>
<!-- svelte-ignore a11y_autofocus -->
<input
id="access_passcode_input"
bind:value={entered_passcode}
class="input w-32 transition-all"
class:hidden={!show_passcode_input}
type="text"
placeholder="Passcode"
autofocus={show_passcode_input}
/>
<!-- <div class="current_text transition-all">{$ae_loc.access_type}</div> -->
</span>
{/if}
</div>
</section>
<style lang="scss">
/* BEGIN: AE's Svelte Quick Access Type component */
/*
#xxx-AE-Quick-Access-Type {
// position: absolute;
position: fixed;
// position: relative;
// position: static;
// position: sticky;
// top: 1em;
bottom: 1.5rem;
right: 0rem;
padding: .5rem;
// lightyellow
// background-color: hsla(60,100%,90%,.30);
// background-color: rgba(var(--color-surface-500) / .5);
border-top: solid thin hsla(0,0%,0%,.25);
border-left: solid thin hsla(0,0%,0%,.25);
border-bottom: solid thin hsla(0,0%,0%,.25);
border-top-left-radius: .5em;
border-bottom-left-radius: .5em;
opacity: .15;
// opacity: 1;
font-size: .75rem;
z-index: 5;
// NOTE: transition when no longer hovering
transition-property: opacity, background-color;
transition-delay: 1.25s;
transition-duration: .75s;
transition-timing-function: ease-out;
}
*/
/*
#xxx-AE-Quick-Access-Type:hover {
border-top: solid thin hsla(0,0%,0%,.95);
border-left: solid thin hsla(0,0%,0%,.95);
border-bottom: solid thin hsla(0,0%,0%,.95);
opacity: 1;
// NOTE: transition when hover starts
transition-property: opacity, background-color;
transition-delay: .5s;
transition-duration: .25s;
transition-timing-function: ease-in;
}
*/
/* #Access-Type .unlock_text {
transition: width 2s, height 2s, background-color 2s, transform 2s;
} */
/* END: Svelte Access Type component */
.access_type_unlock_btn:hover .lock_icon {
display: none;
}
.access_type_unlock_btn:hover .unlock_icon {
display: initial;
}
.access_type_unlock_btn .unlock_text {
display: none;
}
.access_type_unlock_btn:hover .unlock_text {
display: initial;
/* outline: solid thin red; */
}
/*
.ae_access_type .current_text {
display: none;
}
.ae_access_type:hover .current_text {
display: initial;
outline: solid thin red;
}
*/
</style>

View File

@@ -0,0 +1,352 @@
<script lang="ts">
import {
Settings
} from '@lucide/svelte';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
// import Element_theme from '$lib/e_app_theme.svelte';
let notes: null|string = null;
let all: boolean = false;
interface Props {
log_lvl?: number;
hide?: null|boolean;
expand?: boolean;
// export let theme_mode: null|string = null;
// set_theme_mode?: null|boolean;
// export let theme_name: null|string = null;
// set_theme_name?: null|boolean;
}
let {
log_lvl = $bindable(0),
hide = $bindable(false),
expand = $bindable(false),
// set_theme_mode = null,
// set_theme_name = null
}: Props = $props();
// const dispatch = createEventDispatcher();
// onMount(() => {
// // console.log('** Element Mounted: ** Element App Config');
// if (set_theme_mode) {
// $slct_trigger = 'set_theme_mode';
// }
// if (set_theme_name) {
// $slct_trigger = 'set_theme_name';
// }
// });
// $: if ($slct_trigger == 'set_theme_mode' && $ae_loc?.app_cfg?.theme_mode) {
// console.log(`$ae_loc.app_cfg.theme_mode=${$ae_loc?.app_cfg?.theme_mode}`);
// $slct_trigger = null;
// if ($ae_loc.app_cfg.theme_mode == 'light') {
// document.documentElement.classList.remove('dark');
// document.documentElement.classList.add('light');
// } else if ($ae_loc.app_cfg.theme_mode == 'dark') {
// document.documentElement.classList.remove('light');
// document.documentElement.classList.add('dark');
// }
// }
// $: if ($slct_trigger == 'set_theme_name' && $ae_loc?.app_cfg?.theme_name) {
// console.log(`$ae_loc?.app_cfg?.theme_name=${$ae_loc?.app_cfg?.theme_name}`);
// $slct_trigger = null;
// // Update the body attribute named "data-theme" to the current theme name.
// document.body.setAttribute('data-theme', $ae_loc?.app_cfg?.theme_name);
// }
// $: if (entered_passcode && entered_passcode.length >= 5) {
// console.log(`entered_passcode=${entered_passcode}`);
// handle_check_access_type_passcode();
// }
// $: if (trigger && $ae_loc.access_type) {
// console.log(`$ae_loc.access_type=${$ae_loc.access_type}`);
// let access_checks_results = ae_util.process_permission_checks($ae_loc.access_type);
// $ae_loc = {...$ae_loc, ...access_checks_results};
// } else if (trigger) {
// console.log(`$ae_loc.access_type=not set`);
// // Send an empty string to reset the permissions. This is the same as sending 'anonymous'.
// let access_checks_results = ae_util.process_permission_checks('');
// $ae_loc = {...$ae_loc, ...access_checks_results};
// }
function handle_something() {
// console.log('*** handle_something() ***');
}
function handle_clear_storage(item: null|string) {
// console.log('*** handle_clear_storage() ***');
// window.localStorage.setItem('access_type', 'anonymous');
// return true;
}
// function dispatch_something_changed() {
// console.log('*** dispatch_something_changed() ***');
// console.log(ae_util);
// console.log($ae_loc);
// dispatch('access_type_changed', {
// access_type: $ae_loc.access_type
// });
// }
</script>
<!-- transition duration-500 delay-1000 hover:duration-500 hover:delay-1000 hover:transition-all -->
<section
id="AE-App-Cfg"
class="
ae_app_cfg
hidden-print
bg-blue-100 text-gray-900
dark:bg-blue-800 dark:text-gray-200
flex flex-col flex-wrap gap-1
items-end justify-center
w-72 max-w-72
p-1
border-2 border-gray-200
duration-300 delay-150 hover:delay-1000 hover:ease-out
transition-all
"
class:hidden={hide}
>
<header
class:hidden={!expand}
class="ae_header w-full"
>
<h2 class="text-sm text-center font-semibold">
Config
</h2>
</header>
<div
class="ae_cfg_content text-xs space-y-4 my-4"
class:hidden={!expand}
data-sveltekit-preload-data="false"
>
<section class="space-y-2">
<div>
<h2 class="strong">Access Type:</h2>
</div>
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
{#if $ae_loc.access_type == 'super'}
<span class="fas fa-secret mx-1"></span> Super Access
{:else if $ae_loc.access_type == 'manager'}
<span class="fas fa-user-shield mx-1"></span> Manager Access
{:else if $ae_loc.access_type == 'administrator'}
<span class="fas fa-user-ninja mx-1"></span> Administrator Access
{:else if $ae_loc.access_type == 'trusted'}
<span class="fas fa-user-nurse mx-1"></span> Trusted Access
{:else if $ae_loc.access_type == 'authenticated'}
<span class="fas fa-user-friends mx-1"></span> Authenticated Access
{:else if $ae_loc.access_type == 'anonymous'}
<span class="fas fa-users mx-1"></span> Anonymous Access
{:else}
<span class="fas fa-unlock mx-1"></span> Unknown Access
{/if}
<!-- <button
class="btn btn-sm variant-glass-secondary access_type_lock_btn hover:transition-all"
on:click={() => {
handle_clear_access();
}}
title="Access mode is currently enabled/unlocked. Click to exit and lock."
>
<span class="fas fa-lock mx-1"></span> Lock
</button> -->
{:else}
Not logged in
{/if}
</section>
<!-- END: Access Type -->
<section class="space-y-2">
<h2 class="strong">Utilities:</h2>
<a
class="btn btn-sm preset-tonal-secondary"
href="/hosted_files"
>
<span class="fas fa-code mx-1"></span>
Util: Convert Videos
</a>
{#if $ae_loc.iframe}
<a
class="btn btn-sm preset-tonal-secondary"
href="/?iframe=false"
>
<span class="fas fa-code mx-1"></span>
Exit iframe Mode
</a>
{:else}
<a
class="btn btn-sm preset-tonal-secondary"
href="/?iframe=true"
>
<span class="fas fa-code mx-1"></span>
Use iframe Mode
</a>
{/if}
<div>
<button
class="btn btn-sm preset-tonal-warning"
title="Reload and clear the page cache"
onclick={() => {
// $ae_loc.
window.location.reload(true);
}}
>
<span class="fas fa-sync mx-1"></span>
Reload
&
<span class="fas fa-trash mx-1"></span>
Clear Cache
</button>
<button
class="btn btn-sm preset-tonal-warning"
title="Clear the browser storage for this page"
onclick={() => {
if (!confirm('Are you sure you want to clear the local and session storage?')) {
return false;
}
// Clear the local and session storage
localStorage.clear();
sessionStorage.clear();
// Clear Indexed DB as well
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db');
window.location.reload();
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
}}
>
<span class="fas fa-eraser mx-1"></span>
Clear Storage & DB
</button>
</div>
</section>
<!-- END: Utilities -->
</div>
<!-- class:justify-between={expand}
class:justify-end={!expand} -->
<div
class="flex flex-row gap-2 items-center justify-between w-full"
>
<!-- {#if !expand} -->
<span>
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
{#if $ae_loc.access_type == 'super'}
<span class="fas fa-secret mx-1"></span> Super Access
{:else if $ae_loc.access_type == 'manager'}
<span class="fas fa-user-shield mx-1"></span> Manager Access
{:else if $ae_loc.access_type == 'administrator'}
<span class="fas fa-user-ninja mx-1"></span> Administrator Access
{:else if $ae_loc.access_type == 'trusted'}
<span class="fas fa-user-nurse mx-1"></span> Trusted Access
{:else if $ae_loc.access_type == 'authenticated'}
<span class="fas fa-user-friends mx-1"></span> Authenticated Access
{:else if $ae_loc.access_type == 'anonymous'}
<span class="fas fa-users mx-1"></span> Anonymous Access
{:else}
<span class="fas fa-unlock mx-1"></span> Unknown Access
{/if}
<!-- <button
class="btn btn-sm variant-glass-secondary access_type_lock_btn hover:transition-all"
on:click={() => {
handle_clear_access();
}}
title="Access mode is currently enabled/unlocked. Click to exit and lock."
>
<span class="fas fa-lock mx-1"></span> Lock
</button> -->
{:else}
Not logged in
{/if}
</span>
<!-- {/if} -->
<button
class="
ae_cfg_btn
btn btn-sm text-sm
preset-tonal-warning
group transition-all
"
onclick={() => {
expand = !expand;
}}
>
<!-- <span class="fas fa-cog m-1"></span> -->
<span class="inline-block" title="Settings">
<Settings class="m-1" />
</span>
<span
class="
cfg_text
hidden
group-hover:inline
"
>
Settings
</span>
</button>
</div>
</section>
<style lang="postcss">
.ae_cfg_btn .cfg_text {
/* display: none; */
}
.ae_cfg_btn:hover .cfg_text {
/* display: initial; */
/* outline: solid thin red; */
}
/* .access_type .current_text {
display: none;
} */
/* .access_type:hover .current_text {
display: initial;
} */
/* END: AE's Svelte App Config component */
</style>

View File

@@ -0,0 +1,119 @@
<script lang="ts">
interface Props {
children?: import('svelte').Snippet;
log_lvl?: number;
value: any;
success?: boolean;
btn_text?: string;
btn_title?: string;
btn_class?: string;
hide_icon?: boolean;
hide_text?: boolean;
icon_name?: string;
}
let {
children,
log_lvl = 0,
value = $bindable(''),
success = $bindable(false),
btn_text = 'Copy to Clipboard',
btn_title = 'Copy to Clipboard',
btn_class = 'btn btn-sm preset-tonal-warning text-warning-500 m-1',
hide_icon = false,
hide_text = false,
icon_name = 'copy', // copy, check, link
}: Props = $props();
// *** Import Svelte specific
// import { browser } from '$app/environment';
// *** Import other supporting libraries
import {
// ArrowBigRight,
// CircleX,
CircleCheck,
Copy,
// Eye, EyeOff,
// Key,
Link,
// LogIn, LogOut, LockKeyhole,
// Mail, MailCheck,
// Menu,
// RefreshCw, RefreshCcw, RefreshCcwDot,
// ShieldEllipsis, ShieldMinus, ShieldPlus, ShieldUser,
// User, UserCheck
} from '@lucide/svelte';
if (log_lvl) {
console.log(`Clipboard component initialized with value:`, value);
}
// Select your trigger element
// const elemButton: HTMLButtonElement | null = document.querySelector('[data-button]');
// Add a click event handler to the trigger
// elemButton?.addEventListener('click', () => {
// // Call the Clipboard API
// navigator.clipboard
// // Use the `writeText` method write content to the clipboard
// .writeText(value)
// // Handle confirmation
// .then(() => {
// if (log_lvl) {
// console.log(`Clipboard write successful: ${value}`);
// }
// success = true;
// });
// });
</script>
<button
type="button"
data-button
onclick={() => {
// if (browser) {
// Call the Clipboard API
navigator.clipboard
// Use the `writeText` method write content to the clipboard
.writeText(value)
// Handle confirmation
.then(() => {
if (log_lvl) {
console.log(`Clipboard write successful: ${value}`);
}
success = true;
});
// } else {
// if (log_lvl) {
// console.log(`Clipboard write attempted in non-browser environment.`);
// }
// }
}}
class={btn_class}
title={btn_title}
>
<!-- {@render btn_text} -->
{#if icon_name === 'link'}
<Link
class="mx-1 {hide_icon ? 'hidden' : 'inline-block' }"
size="1.2em"
/>
{:else if icon_name === 'check'}
<CircleCheck
class="mx-1 {hide_icon ? 'hidden' : 'inline-block' }"
size="1.2em"
/>
{:else}
<Copy
class="mx-1 {hide_icon ? 'hidden' : 'inline-block' }"
size="1.2em"
/>
{/if}
<span class="{hide_text ? 'hidden' : 'inline-block' }">
{btn_text}
</span>
{@render children?.()}
</button>

View File

@@ -0,0 +1,294 @@
<script lang="ts">
// This will be the wrapper for the CodeMirror editor. It should hide most of the configuration requirements.
// WARNING: This has not been fully updated to Svelte version 5. It is a work in progress.
// *** Import Svelte version 5 specific
import { browser } from '$app/environment';
import { onMount, onDestroy } from 'svelte';
import {
EditorView, keymap, highlightSpecialChars, drawSelection,
highlightActiveLine, dropCursor, rectangularSelection,
crosshairCursor,
gutter, GutterMarker, highlightActiveLineGutter, lineNumbers,
placeholder as placeholderExt,
} from "@codemirror/view"
import { EditorState, RangeSet, StateEffect, type Extension, Text } from '@codemirror/state';
import {
defaultKeymap, history, historyKeymap, indentWithTab,
} from "@codemirror/commands"
import { indentUnit } from '@codemirror/language';
import { languages } from '@codemirror/language-data';
// import {
// defaultHighlightStyle, syntaxHighlighting, indentOnInput,
// bracketMatching, foldGutter, foldKeymap
// } from "@codemirror/language"
// import {
// defaultHighlightStyle, syntaxHighlighting, indentOnInput,
// bracketMatching, foldGutter, foldKeymap
// } from "@codemirror/language"
// import {
// searchKeymap, highlightSelectionMatches
// } from "@codemirror/search"
// import {
// autocompletion, completionKeymap, closeBrackets,
// closeBracketsKeymap
// } from "@codemirror/autocomplete"
// import {lintKeymap} from "@codemirror/lint"
// import { } from '@codemirror/gutter'; // Merged into @codemirror/view
import { basicSetup } from 'codemirror';
import { markdown, markdownLanguage } from '@codemirror/lang-markdown';
// import { css } from '@codemirror/lang-css';
// import { javascript } from '@codemirror/lang-javascript';
// import { json } from '@codemirror/lang-json';
// import { html } from '@codemirror/lang-html';
import { oneDark } from "@codemirror/theme-one-dark";
// Props
export let content: string = 'test test test test';
export let new_content: string = '';
// export let language: Extension = markdown(); // javascript()
export let theme_mode: string = 'light'; // 'dark' | 'light'
export let theme: Extension = EditorView.baseTheme(); // EditorView.baseTheme(); // oneDark
export let extensions: Extension[] = [];
export let editable: boolean = true;
export let readonly: boolean = false;
export let placeholder: string = 'Start typing...';
export let show_line_numbers: boolean = false;
export let wrap_lines: boolean = true;
export let use_tab: boolean = true;
export let tab_size: number = 4;
let classes = "";
export { classes as class };
let editor_element: HTMLDivElement;
let editorView: EditorView;
// theme = [
// // baseTheme,
// theme,
// ];
// console.log(`Theme:`, theme);
if (theme_mode == 'dark') {
theme = oneDark;
} else {
// theme = EditorView.baseTheme({
// "&": {
// color: "black",
// backgroundColor: "white"
// },
// ".cm-cursor, .cm-dropCursor": { borderLeftColor: "#000" },
// "&.cm-focused .cm-selectionBackground, ::selection": {
// backgroundColor: "#B7D5FF"
// },
// "&.cm-focused .cm-selectionForeground": { color: "black" }
// });
}
if (editable) {
extensions.push(EditorView.editable.of(true));
} else {
// extensions.push(EditorState.editable.of(false));
}
if (readonly) {
extensions.push(EditorState.readOnly.of(true));
} else {
// extensions.push(EditorState.readOnly.of(false));
}
if (placeholder) { extensions.push(placeholderExt(placeholder)); }
if (show_line_numbers) {
// extensions.push(lineNumbers({ class: "line-numbers" }));
} else {
// extensions.push(gutter({ class: "hidden-gutter" }));
// extensions.push(lineNumbers(false));
// extensions.push(gutter(false));
// extensions.pop();
// extensions.slice(extensions.indexOf(lineNumbers), 1);
}
if (wrap_lines) { extensions.push(EditorView.lineWrapping); }
if (use_tab) { extensions.push(keymap.of([indentWithTab])); }
if (tab_size) { extensions.push(indentUnit.of(" ".repeat(tab_size))); }
// Enable spell check
extensions.push(EditorView.contentAttributes.of({ spellcheck: "true" }));
// let languages = [
// { name: 'CSS', mode: 'css' },
// { name: 'HTML', mode: 'html' },
// { name: 'JavaScript', mode: 'javascript' },
// { name: 'Markdown', mode: 'markdown' },
// { name: 'Python', mode: 'python' },
// ];
// Reactive declaration for extensions
$: editor_extensions = [
basicSetup,
// gutter({class: "hidden-gutter"}),
// A line number gutter
lineNumbers(false),
// A gutter with code folding markers
// foldGutter(false),
// lineWrapping(false),
// EditorView.lineWrapping, // Enable line wrapping
// EditorView.indentUnit.of(" ".repeat(tab_size)),
// EditorView.tabSize.of(tab_size),
// indentUnit.of(" ".repeat(tab_size)),
// EditorView.editable.of(editable),
// EditorState.readOnly.of(readonly),
// EditorState.tabSize.of(tab_size),
// keymap.of([indentWithTab]),
// placeholderExt(placeholder),
// language,
markdown({base: markdownLanguage, codeLanguages: languages}),
// javascript({typescript: true}),
// json(),
// css(),
// html(),
theme,
...extensions
];
// baseTheme = {
// ".cm-o-replacement": {
// display: "inline-block",
// width: ".5em",
// height: ".5em",
// borderRadius: ".25em"
// },
// "&light .cm-o-replacement": {
// backgroundColor: "#04c"
// },
// "&dark .cm-o-replacement": {
// backgroundColor: "#5bf"
// }
// },
// let dimensions = {
// width: '100%',
// height: 'calc(100vh - 48px)' // Adjust for header and other elements
// };
// editorView.setSize(dimensions.width, dimensions.height);
// Initialize CodeMirror on mount
onMount(() => {
editorView = new EditorView({
state: EditorState.create({
doc: content,
extensions: editor_extensions,
}),
parent: editor_element, // document.body
dispatch: (transaction) => {
editorView.update([transaction]);
if (transaction.docChanged) {
new_content = editorView.state.doc.toString();
const newContent = editorView.state.doc.toString();
// dispatch('change', newContent);
}
}
});
});
// Clean up on destroy
onDestroy(() => {
editorView?.destroy();
});
// Update editor content when `content` prop changes
$: if (editorView && editorView.state.doc.toString() !== content) {
editorView.setState(
EditorState.create({
doc: content,
extensions: editor_extensions
})
);
};
</script>
{#if browser}
<!-- flex flex-col flex-wrap items-center justify-center -->
<div bind:this={editor_element} class="codemirror-wrapper h-100 max-h-full w-100 max-w-6xl {classes}"></div>
{:else}
<div class="scm-waiting {classes}">
<div class="scm-waiting__loading scm-loading">
<p class="scm-loading__text">Loading editor...</p>
</div>
<pre class="scm-pre cm-editor">{content}</pre>
</div>
{/if}
<style>
/* .codemirror-wrapper :global(.cm-focused) {
outline: none;
} */
.codemirror-wrapper {
flex-grow: 1;
/* flex-shrink: 1; */
/* flex-basis: 100%; */
/* font-size: 1rem; */
width: 100%;
max-width: 100%;
height: 100%;
max-height: 100%;
/* overflow: auto; */
/* background-color: var(--cm-background); */
/* text-wrap: normal; */
/* text-wrap: balance; */
/* text-wrap: wrap; */
/* text-wrap: break-word; */
}
.codemirror-wrapper :global(.cm-editor) {
/* font-size: .8rem; */
/* text-wrap: normal; */
/* text-wrap: balance; */
/* text-wrap: wrap; */
/* text-wrap: break-word; */
/* max-width: 100%; */
/* max-height: 100%; */
/* overflow: auto; */
/* width: 100%; */
/* height: 100%; */
/* background-color: var(--cm-background); */
/* color: var(--cm-text); */
}
/* .codemirror-wrapper :global(.cm-gutters) {
background-color: var(--cm-gutter-background);
color: var(--cm-gutter-text);
}
.codemirror-wrapper :global(.cm-gutter) {
background-color: var(--cm-gutter-background);
color: var(--cm-gutter-text);
}
.codemirror-wrapper :global(.cm-gutterElement) {
background-color: var(--cm-gutter-background);
color: var(--cm-gutter-text);
} */
</style>

View File

@@ -0,0 +1,219 @@
<script lang="ts">
// *** Import Svelte specific
// *** Import other supporting libraries
import {
Bug,
CircleX, Info,
ToggleLeft, ToggleRight,
X
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
// import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
// *** Setup Svelte properties
interface Props {
log_lvl?: number;
hide?: null|boolean;
expand?: boolean;
}
let {
log_lvl = $bindable(0),
hide = $bindable(false),
expand = $bindable(false),
}: Props = $props();
</script>
<!-- App Debug Menu -->
<!-- opacity-25
hover:opacity-100 -->
<!-- text-slate-400 hover:text-slate-800 -->
<section
class="
ae_app__debug_menu
hidden-print
z-50
absolute bottom-0 left-0
flex
text-sm sm:text-sm md:text-md lg:text-md xl:text-md 2xl:text-lg
dark:text-slate-400 dark:hover:text-slate-200
bg-red-100/60 dark:bg-red-800/50
hover:bg-red-200 hover:dark:bg-red-900
mx-1 my-2
max-h-min
max-w-lg
transition-all
transition-delay-1000
transition-duration-1000
ease-in
border-2
border-red-300 dark:border-red-700
hover:border-red-500 hover:dark:border-red-500
"
class:top-0={expand}
class:w-full={expand}
class:hidden={hide}
class:border-transparent={!expand}
class:dark:border-transparent={!expand}
class:hover:border-transparent={!expand}
class:hover:bg-transparent={!expand}
>
<div
class:hidden={!expand}
class:border-red-200={expand}
class:dark:border-red-800={expand}
class="
transition-all
transition-delay-1000
transition-duration-1000
ease-in
overflow-y-auto
px-1 py-4
opacity-50
hover:opacity-100
relative
"
>
<!-- flex flex-col items-center justify-center max-h-full outline -->
<div>
<!-- <span class="fas fa-bug mx-1"></span> -->
<Bug class="inline-block mx-1" />
<span>Debug</span>
</div>
<pre class="text-xs">
{JSON.stringify($ae_loc, null, 2)}
</pre>
</div>
<span class="absolute top-0 right-0 flex flex-row gap-1 items-center justify-center">
<button
type="button"
onclick={() => {
console.log('Debug ae_loc:', $ae_loc);
$ae_loc.debug_mode = !$ae_loc?.debug_mode;
}}
class="
btn btn-sm
preset-outlined-surface-400-600 preset-filled-suface-200-800
hover:preset-tonal-success
transition-all
"
title="Turn debug content and styles off and on"
>
{#if $ae_loc?.debug_mode}
<!-- <span class="fas fa-toggle-on mx-1"></span> -->
<ToggleRight strokeWidth="2.5" color="green" class="inline-block mx-1" />
<span>Debug</span>
<span class="hidden">
Mode On
</span>
{:else}
<!-- <span class="fas fa-toggle-off mx-1"></span> -->
<ToggleLeft strokeWidth="1" color="gray" class="inline-block mx-1" />
<span>Debug?</span>
<span class="hidden">
Mode Off
</span>
{/if}
<!-- <span class="fas fa-toggle-on mx-1"></span> -->
<!-- <ToggleRight class="inline-block mx-1" /> -->
<!-- <X class="inline-block mx-1" /> -->
</button>
<button
type="button"
onclick={() => {
if (log_lvl) {
console.log('Showing quick info/debug menu.');
}
expand = false;
$ae_sess.debug_menu.hide_quick_info = false;
}}
title="Show Quick Info"
class="
btn btn-sm
preset-outlined-surface-400-600 preset-filled-suface-200-800
hover:preset-tonal-success
transition-all
"
>
<!-- <span class="fas fa-info-circle mx-1"></span> -->
<Info class="inline-block mx-1" />
Quick Info
</button>
<button
type="button"
onclick={() => {
console.log('Debug ae_loc:', $ae_loc);
// $ae_loc.debug_menu.expand = !$ae_loc?.debug_menu?.expand;
expand = !expand;
}}
class="
btn btn-sm
preset-outlined-surface-400-600 preset-filled-suface-200-800
hover:preset-tonal-warning
transition-all
"
title="Turn debug content and styles off and on"
>
<!-- <span class="fas fa-toggle-on mx-1"></span> -->
<!-- <ToggleRight class="inline-block mx-1" /> -->
<CircleX class="inline-block mx-1" />
<span class="hidden">
Close
</span>
<span>Debug</span>
</button>
</span>
<button
type="button"
onclick={() => {
console.log('Debug ae_loc:', $ae_loc);
// $ae_loc.debug_menu.expand = !$ae_loc?.debug_menu?.expand;
expand = !expand;
}}
id="AE-Quick-Debug"
class="
btn btn-icon
text-xs
p-1
preset-outlined-surface-100-900
hover:preset-filled-warning-100-900
transition-all
fixed bottom-8 left-2
text-neutral-300 hover:text-neutral-800
dark:text-neutral-700 dark:hover:text-neutral-200
"
title="Turn debug content and styles off and on"
>
<!-- absolute bottom-2 left-2 -->
<!-- fixed bottom-0 left-0 -->
&pi;
</button>
</section>

View File

@@ -0,0 +1,525 @@
<script lang="ts">
interface Props {
log_lvl?: number;
additional_kv?: key_val;
e_success?: boolean;
e_class?: string;
e_title?: string;
e_text?: string;
e_class_h1?: string;
e_class_h2?: string;
e_class_form_hidden?: string;
e_class_form_showing?: string;
btn_text?: string;
btn_title?: string;
btn_class?: string;
show_btn_class?: string;
hide_icon?: boolean;
}
let {
log_lvl = 0,
additional_kv = $bindable({}),
e_success = $bindable(false),
e_class = '',
e_title = 'Technical Help',
e_text = 'Request technical help for this application.',
e_class_h1 = $bindable(''),
e_class_h2 = $bindable(''),
e_class_form_hidden = $bindable(''),
e_class_form_showing = $bindable(''),
btn_text = 'Technical Help',
btn_title = 'Technical support help',
btn_class = '',
show_btn_class = '',
hide_icon = false,
}: Props = $props();
// *** Import Svelte specific
import { goto } from '$app/navigation';
// *** Import other supporting libraries
import {
// ArrowBigRight,
BadgeQuestionMark,
ChevronDown, ChevronRight,
// CircleX,
// Copy,
// Eye, EyeOff,
// Key,
LifeBuoy,
// LogIn, LogOut, LockKeyhole,
// Mail, MailCheck,
// Menu,
RefreshCw, RefreshCcw, RefreshCcwDot,
// ShieldEllipsis, ShieldMinus, ShieldPlus, ShieldUser,
SquareX,
// User, UserCheck
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import { ae_snip, ae_loc, ae_sess, ae_api, ae_trig, slct, slct_trigger, type key_val } from '$lib/ae_stores';
import { User } from 'lucide-svelte';
import { api } from './api';
if (log_lvl) {
console.log(`Help - technical support component loaded`);
}
let help_tech_text: string = $state('');
let hide_additional_info: boolean = $state(true);
function preventDefault(fn) {
return function (event) {
event.preventDefault();
fn.call(this, event);
};
}
function send_help_tech_email() {
if (log_lvl) {
console.log(`*** send_help_tech_email() ***`);
}
let subject = `Technical Notification - ${$ae_loc.name}`;
let body_html = `
<div>
Technical Notification,\n\n
<ul>
<li>Datetime: ${new Date().toISOString()}</li>
<li>URL: ${window.location.href}</li>
<li>Browser: ${navigator.userAgent}</li>
<li>Viewport Size: ${window.innerWidth} x ${window.innerHeight}</li>
<li>Screen Resolution: ${window.screen.width} x ${window.screen.height}</li>
<li>In iframe: ${$ae_loc?.iframe}</li>
<li>Theme Mode: ${$ae_loc?.theme_mode}</li>
<li>Theme Name: ${$ae_loc?.theme_name}</li>
<li>Account ID: ${$slct.account_id}</li>
<li>Access Type: ${$ae_loc?.access_type}</li>
${$ae_loc?.person_id ? `<li>person_id: ${$ae_loc?.person_id}</li>` : ''}
${$ae_loc?.user_id ? `<li>user_id: ${$ae_loc?.user_id}</li>` : ''}
${$ae_loc?.username ? `<li>username: ${$ae_loc?.username}</li>` : ''}
${$ae_loc?.email ? `<li>email: ${$ae_loc?.email}</li>` : ''}
<li>API Base URL: ${$ae_api.base_url}</li>
</ul>
<p>
Help Request:\n\n
${help_tech_text}
</p>
</div>
`;
api.send_email({
api_cfg: $ae_api,
from_email: $ae_loc.site_cfg_json?.noreply_email ?? 'noreply+tech@oneskyit.com',
from_name: $ae_loc.site_cfg_json?.noreply_name ?? 'IT NoReply',
// to_email: $ae_loc.site_cfg_json?.admin_email ?? 'admin+tech@oneskyit.com', // 'scott+idaabb@oneskyit.com',
to_email: 'it+tech@oneskyit.com',
// to_email: $idaa_slct.post_obj.email,
// to_email: 'scott+idaabb@oneskyit.com',
// to_name: $ae_loc.site_cfg_json?.admin_name ?? 'IT Tech',
to_name: 'IT Tech',
// to_name: $idaa_slct.post_obj.full_name ?? 'IDAA BB Poster',
subject: subject,
body_html: body_html,
});
help_tech_text = ''; // Clear the text area after sending
}
</script>
<!-- class:bg-radial-[at_55%_50%]={$ae_sess.show_help_tech}
class:from-blue-400={$ae_sess.show_help_tech}
class:to-transparent={$ae_sess.show_help_tech}
class:to-90%={$ae_sess.show_help_tech} -->
<div
class="
flex flex-row
items-center justify-center
rounded-lg shadow-2xl
border-1 border-transparent
transition-all
{e_class}
{!$ae_sess.show_help_tech ? e_class_form_hidden : e_class_form_showing}
relative
"
class:w-xl={$ae_sess.show_help_tech}
class:w-fit={!$ae_sess.show_help_tech}
class:mx-auto={$ae_sess.show_help_tech}
class:m-2={$ae_sess.show_help_tech}
class:p-2={$ae_sess.show_help_tech}
class:hover:border-blue-400={$ae_sess.show_help_tech}
class:hover:dark:border-blue-600={$ae_sess.show_help_tech}
class:hover:shadow-blue-200={$ae_sess.show_help_tech}
class:hover:dark:shadow-blue-800={$ae_sess.show_help_tech}
class:bg-blue-100={$ae_sess.show_help_tech}
class:dark:bg-blue-900={$ae_sess.show_help_tech}
>
{#if $ae_sess.show_help_tech}
<div
class="
w-xl
flex flex-col gap-1
items-center justify-center
p-2
border rounded-xl border-gray-500/20
bg-blue-200
dark:bg-blue-800
transition-all
"
>
<div
class="
d-flex align-items-center justify-content-between
flex flex-row gap-1 items-center justify-between
w-full
"
>
<h1
class="
h1
text-base font-semibold text-gray-800 dark:text-gray-200
w-fit
{e_class_h1}
"
>
{#if e_success}
<span class="text-lg text-green-800 dark:text-green-200 font-semibold">
<BadgeQuestionMark class="inline-block mr-2" />
Help Requested
</span>
{:else}
<span class="text-lg text-gray-800 dark:text-gray-200 font-semibold">
<BadgeQuestionMark class="inline-block mr-2" />
<!-- Request Technical Help -->
Notify Technical Support
</span>
<!-- <span class="text-gray-500">
Send your help request with or without a description.
</span> -->
{/if}
<!-- Cancel button -->
</h1>
<button
type="button"
onclick={() => ($ae_sess.show_help_tech = false)}
class="
btn btn-base
preset-tonal-tertiary
preset-outlined-tertiary-100-900
hover:preset-filled-tertiary-200-800
transition-all
{btn_class}
"
title="Close Help Request Form"
>
<!-- <span class="fas fa-times"></span> -->
<SquareX size="1em" />
<span class="sr-only">Close</span>
</button>
</div>
<form
class="
m-1
flex flex-col gap-1
items-center justify-center
w-md max-w-lg
"
onsubmit={preventDefault(() => {
// Do stuff...
send_help_tech_email();
// Hide the request form
$ae_sess.show_help_tech = false;
alert('Notification sent to the IT team.');
})}
>
<textarea
class="
form-control
w-full max-w-lg h-24 p-2
border border-gray-300 rounded
text-gray-950 dark:text-gray-50
bg-white dark:bg-gray-500
hover:dark:bg-gray-50
hover:dark:text-gray-950
"
placeholder="Send with or without a description...."
bind:value={help_tech_text}
></textarea>
<button
type="submit"
class="
btn btn-lg
m-1
preset-tonal-warning
preset-outlined-warning-100-900
hover:preset-filled-warning-200-800
transition-all
{btn_class}
"
title={btn_title}
>
{#if !hide_icon}
<!-- <BadgeQuestionMark class="inline-block mr-2" /> -->
<LifeBuoy class="inline-block m-1" />
{/if}
{#if !help_tech_text}
Notify Without Description
{:else}
Send Notification
{/if}
</button>
</form>
<div
class="
text-sm text-gray-700 dark:text-gray-300 text-center italic
"
>
This is intended for technical issues only. Please contact your organization's staff if you have a question about your membership, recorded content, meetings, or posts.
</div>
<div
class="
border border-gray-300 rounded p-2
w-full
max-w-2xl
overflow-scroll
"
>
<div
class="
d-flex align-items-center justify-content-between
flex flex-row gap-1 items-center justify-between
w-full
"
>
<h2
class="
h2
text-base font-semibold text-gray-800 dark:text-gray-200
{e_class_h2}
"
>
<span class="text-base font-semibold text-gray-800 dark:text-gray-200">
Additional Information Included
</span>
</h2>
<!-- Button to expand and show additional information -->
<button
type="button"
class="
btn btn-sm preset-tonal-tertiary
{btn_class}
"
onclick={() => (hide_additional_info = !hide_additional_info)}
title="Toggle additional information"
>
<span>
{#if hide_additional_info}
<!-- <span class="fas fa-caret-right"></span> -->
<ChevronRight size="1em" class="inline-block" />
Show
{:else}
<!-- <span class="fas fa-caret-down"></span> -->
<ChevronDown size="1em" class="inline-block" />
Hide
{/if}
</span>
</button>
</div>
<ul
class="list-disc list-inside text-sm text-gray-800 dark:text-gray-200"
class:hidden={hide_additional_info}
>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Datetime =</span> {new Date().toISOString()}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">URL =</span> {window.location.href}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Browser =</span> {navigator.userAgent}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Viewport Size =</span> {window.innerWidth} x {window.innerHeight}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Screen Resolution =</span> {window.screen.width} x {window.screen.height}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Dark mode =</span> {window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ?? false}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">In iframe =</span> {$ae_loc?.iframe}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Theme Mode =</span> {$ae_loc?.theme_mode}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Theme Name =</span> {$ae_loc?.theme_name}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Account ID =</span> {$slct.account_id}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">Access Type =</span> {$ae_loc?.access_type}</li>
{#if $ae_loc?.person_id}
<li><span class="text-sm text-gray-500 dark:text-gray-400">person_id =</span> {$ae_loc?.person_id}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">full_name =</span> {$ae_loc?.full_name}</li>
{/if}
{#if $ae_loc?.user_id}
<li><span class="text-sm text-gray-500 dark:text-gray-400">user_id =</span> {$ae_loc?.user_id}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">username =</span> {$ae_loc?.username}</li>
<li><span class="text-sm text-gray-500 dark:text-gray-400">email =</span> {$ae_loc?.email}</li>
{/if}
<li><span class="text-sm text-gray-500 dark:text-gray-400">API Base URL =</span> {$ae_api.base_url}</li>
{#if additional_kv && Object.keys(additional_kv).length > 0}
<h2 class="text-base font-semibold text-gray-800">Component Info:</h2>
<ul class="list-disc list-inside text-gray-800 text-sm">
{#each Object.entries(additional_kv) as [key, value]}
<li><span class="text-sm text-gray-500 dark:text-gray-400">{key} =</span> {value ?? '-- not set --'}</li>
{/each}
</ul>
{/if}
</ul>
<div class="text-sm text-gray-700 dark:text-gray-300 text-center italic">
This information will be included in the help request to assist technical support in diagnosing the issue.
</div>
</div>
<div
class="
flex flex-row gap-2 items-center justify-around
w-full
mt-2
"
>
<button
type="button"
onclick={() => {
if ($ae_loc.edit_mode) {
// Confirm before clearing
if (!confirm("Are you sure you want to clear IndexedDB databases, localStorage, and sessionStorage? This will also reload the page.")) {
return;
}
console.log("Clearing IndexedDB, localStorage, sessionStorage, and reloading the page...");
// Clear Indexed DB
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db'); // Events module
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
// Clear localStorage and sessionStorage
// Clearing the localStorage will force it to be re-created.
localStorage.clear();
sessionStorage.clear();
goto('/', {invalidateAll: true});
// window.location.reload(true);
} else {
// Confirm before clearing
if (!confirm("Are you sure you want to clear IndexedDB databases and some caches? This will also reload the page.")) {
return;
}
console.log("Clearing IndexedDB, localStorage, sessionStorage, and reloading the page...");
// Clear Indexed DB
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db'); // Events module
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
window.location.reload(true);
}
// This does not seem to work fast enough or something?
// goto('/', {invalidateAll: true});
// The page does usually seem to reload correctly?
// window.location.reload(true); // true only works with Firefox
// alert('Local and Session Storage cleared and Indexed DBs deleted. You will probably want to refresh the page.');
}}
class="
btn btn-sm
preset-tonal-surface
preset-outlined-warning-100-900
hover:preset-filled-warning-200-800
transition-all
{btn_class}
"
title="Clear App Data & Settings: Clear IndexedDB and reload. If in edit mode localStorage and sessionStorage will also be cleared."
>
<!-- <span class="fas fa-eraser mx-1"></span> -->
<!-- <span class="fas fa-sync mx-1"></span> -->
<RefreshCw size="1em" class="inline-block" />
<span class="md:inline">Clear & Reload</span>
</button>
<!-- Cancel button -->
<button
type="button"
onclick={() => ($ae_sess.show_help_tech = false)}
class="
btn btn-sm
preset-tonal-tertiary
preset-outlined-tertiary-100-900
hover:preset-filled-tertiary-100-900
transition-all
{btn_class}
"
title="Close Help Request Form"
>
<!-- <span class="fas fa-times"></span> -->
<SquareX size="1em" class="inline-block" />
<span class="">Cancel</span>
</button>
</div>
</div>
{:else}
<button
type="button"
onclick={() => ($ae_sess.show_help_tech = true)}
class="
btn btn-sm
preset-filled-tertiary-400-600
preset-outlined-tertiary-100-900
hover:preset-filled-warning-500
transition-all
{btn_class}
{show_btn_class}
"
title={btn_title}
>
{#if !hide_icon}
<BadgeQuestionMark class="inline-block m-0.5" />
{/if}
<span class="hidden">
{btn_text}
</span>
</button>
{/if}
</div>

View File

@@ -0,0 +1,928 @@
<script lang="ts">
// *** Import Svelte specific
import { browser } from '$app/environment';
import { goto, invalidateAll } from '$app/navigation';
import { Modal } from 'flowbite-svelte';
// *** Import other supporting libraries
import {
CircleX,
Eye, EyeOff,
Key,
LogIn, LogOut, LockKeyhole,
Mail, MailCheck,
ShieldUser,
User, UserCheck, UserLock
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
import type { key_val } from '$lib/ae_stores';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
import { ae_util } from '$lib/ae_utils/ae_utils';
import { core_func } from '$lib/ae_core/ae_core_functions';
// *** Setup Svelte properties
interface Props {
log_lvl?: number;
data?: any;
hidden: null|boolean;
}
let {
log_lvl = $bindable(0),
data = null,
hidden = $bindable(true),
}: Props = $props();
let url_user_id = data.url.searchParams.get('user_id');
let url_user_key = data.url.searchParams.get('user_key'); // Reminder that "key" is the site's auth key.
let url_user_username = data.url.searchParams.get('username');
let url_user_email = data.url.searchParams.get('user_email');
let ae_promises: key_val = {};
let trigger: null|boolean = $state(null); // Use $state to ensure reactivity
let user_id = $state(null); // Use $state to ensure reactivity
let user_obj: key_val = $state({}); // Use $state to ensure reactivity
let person_id = $state(null); // Use $state to ensure reactivity
let person_obj: key_val = $state({}); // Use $state to ensure reactivity
$effect(() => {
if (user_id && person_id && trigger) {
// alert(`Ready to sign in person! \n\nuser_id: ${user_id}\nperson_id: ${person_id}\n\nThis is a test alert to confirm the values are set.`);
trigger = false;
sign_in();
}
});
let user_email = $state('scott.idem@gmail.com'); // Used for quick lookup of user by email address
let new_password = $state('test12345');
let confirm_password = $state('test12345');
let is_changing_password = $state(false);
let show_password_text = $state('password'); // password or text
function sign_in() {
$ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
$ae_loc.person = person_obj; // Store the full person object for reference
$ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
$ae_loc.user = user_obj; // Store the full user object for reference
if (user_obj.super) {
$ae_loc.access_type = 'super';
} else if (user_obj.manager) {
$ae_loc.access_type = 'manager';
} else if (user_obj.administrator) {
$ae_loc.access_type = 'administrator';
} else if (user_obj.verified) {
// They are "trusted" because the user has been verified. External staff usually start with this level.
$ae_loc.access_type = 'trusted';
} else if (user_obj.public) {
$ae_loc.access_type = 'public';
} else {
$ae_loc.access_type = 'authenticated';
}
$ae_loc.user_access_type = $ae_loc.access_type; // Used to revert back to the user's access type after quick access (temporarily escalate permissions) is turned off.
let access_checks_results = ae_util.process_permission_checks($ae_loc.access_type);
// WARNING: I think this is causing a loop in Svelte or something.
// Last ten effects were:
// Uncaught Svelte error: effect_update_depth_exceeded
// Maximum update depth exceeded. This can happen when a reactive block or effect repeatedly sets a new value. Svelte limits the number of nested updates to prevent infinite loops
// https://svelte.dev/e/effect_update_depth_exceeded
$ae_loc = {
...$ae_loc,
...access_checks_results
};
// $ae_loc = {...access_checks_results};
console.log('Remove the sign in fields from the URL.');
data.url.searchParams.delete('user_id');
data.url.searchParams.delete('user_key');
data.url.searchParams.delete('username');
data.url.searchParams.delete('user_email');
data.url.searchParams.delete('valid_email'); // Part of sign in email for possible future use
let new_url = data.url.toString();
// We need to set browser history and force all load functions to rerun.
goto(new_url, {replaceState: true, invalidateAll: true});
}
function sign_out() {
// Clear the session information
$ae_loc.person_id = null;
$ae_loc.person = {};
$ae_loc.user_id = null;
$ae_loc.user = {};
user_id = null; // Clear the user_id
user_obj = {}; // Clear the user object
person_id = null; // Clear the person_id
person_obj = {}; // Clear the person object
$ae_loc.access_type = ''; // Clear the access type
let access_checks_results = ae_util.process_permission_checks('');
$ae_loc = {...$ae_loc, ...access_checks_results};
$ae_sess.auth__entered_user_id = null;
$ae_sess.auth__entered_user_key = null;
// $ae_sess.auth__entered_username = null; // Keeping the username
$ae_sess.auth__entered_password = null;
indexedDB.deleteDatabase('ae_archives_db'); // Archives module
indexedDB.deleteDatabase('ae_core_db');
indexedDB.deleteDatabase('ae_events_db'); // Events module
indexedDB.deleteDatabase('ae_journals_db'); // Journals module
indexedDB.deleteDatabase('ae_posts_db'); // Posts module
indexedDB.deleteDatabase('ae_sponsorships_db'); // Sponsorships module
// $ae_loc.allow_access = false;
$ae_loc.authenticated_access = false;
$ae_loc.edit_mode = false;
localStorage.clear();
sessionStorage.clear();
console.log('Remove the sign out fields from the URL.');
data.url.searchParams.delete('user_id');
data.url.searchParams.delete('user_key');
data.url.searchParams.delete('username');
data.url.searchParams.delete('user_email');
data.url.searchParams.delete('valid_email'); // Part of sign in email for possible future use
let new_url = data.url.toString();
// We need to set browser history and force all load functions to rerun.
// goto(new_url, {replaceState: true, invalidateAll: true});
// invalidateAll();
window.location.reload();
console.log('Signed out successfully.');
}
if (browser) {
if (url_user_id) {
// Pre-fill the auth__entered_user_id if passed in via the URL.
$ae_sess.auth__entered_user_id = url_user_id;
}
if (url_user_key) {
// Pre-fill the auth__entered_key if passed in via the URL.
$ae_sess.auth__entered_user_key = url_user_key;
}
if (url_user_username) {
// Pre-fill the auth__entered_username if passed in via the URL.
$ae_sess.auth__entered_username = url_user_username;
}
if (url_user_email) {
// Pre-fill the auth__entered_email if passed in via the URL.
$ae_sess.auth__entered_email = url_user_email;
}
}
// Use core_func.send_email_auth_ae_obj__user_id
function handle_send_auth_email({user_id}: {user_id: string}) {
console.log('handle_send_auth_email()');
console.log($ae_loc.base_url); // URL origin
console.log($ae_loc.hostname); // URL hostname
// This function creates a new auth_key and then sends an email to the user with the new auth key.
ae_promises.send_email_auth_ae_obj__user_id = core_func.send_email_auth_ae_obj__user_id({
api_cfg: $ae_api,
account_id: $slct.account_id,
user_id: user_id,
base_url: $ae_loc.base_url,
log_lvl: 2
});
}
// Use core_func.qry_ae_obj_li__user_email
function handle_lookup_user_email({email}: {email: string}) {
console.log('handle_lookup_user_email()');
// If a user is returned then send the new auth key email
ae_promises.load__user_obj_li = core_func.qry_ae_obj_li__user_email({
api_cfg: $ae_api,
account_id: $slct.account_id,
null_account_id: false,
email: email,
log_lvl: 1
}).then((user_response) => {
if (user_response?.user_id_random) {
console.log(`User found for email:`, user_response);
handle_send_auth_email({
user_id: user_response.user_id_random
});
} else if (user_response.length > 0) {
console.log(`Multiple users found for email:`, user_response);
handle_send_auth_email({
user_id: user_response[0].user_id_random
});
} else {
alert('No user found with that email address.');
}
});
}
async function handle_change_password() {
if (!new_password) {
alert('Password cannot be empty.');
return;
}
if (new_password !== confirm_password) {
alert('Passwords do not match.');
return;
}
let use_user_id = $ae_loc.user_id;
let use_password = new_password;
let wait_for_lookup = true;
// Look up the user by email address to get their user ID
if (user_email) {
wait_for_lookup = true;
// ae_promises.load__user_obj_li = await core_func.qry_ae_obj_li__user_email({
// api_cfg: $ae_api,
// account_id: $slct.account_id,
// null_account_id: false,
// email: user_email,
// log_lvl: 1
// }).then((user_response) => {
// if (!user_response) {
// // This means a 404 was returned
// alert('No user found with that email address.');
// return;
// } else if (user_response?.user_id_random) {
// console.log(`User found for email:`, user_response);
// use_user_id = user_response.user_id_random;
// } else if (user_response.length > 0) {
// console.log(`Multiple users found for email:`, user_response);
// use_user_id = user_response[0].user_id_random;
// }
// });
ae_promises.load__user_obj_li = await core_func.qry_ae_obj_li__user_email({
api_cfg: $ae_api,
account_id: $slct.account_id,
null_account_id: false,
email: user_email,
log_lvl: 1
});
if (!ae_promises.load__user_obj_li) {
// This means a 404 was returned
alert('No user found with that email address.');
return;
} else if (ae_promises.load__user_obj_li?.user_id_random) {
console.log(`User found for email:`, ae_promises.load__user_obj_li);
use_user_id = ae_promises.load__user_obj_li.user_id_random;
} else if (ae_promises.load__user_obj_li.length > 0) {
console.log(`Multiple users found for email:`, ae_promises.load__user_obj_li);
use_user_id = ae_promises.load__user_obj_li[0].user_id_random;
}
} else {
wait_for_lookup = false;
}
is_changing_password = true;
if (user_email && wait_for_lookup) {
console.log('Waiting for user lookup to complete...');
// await ae_promises.load__user_obj_li;
}
try {
ae_promises.change_password = await core_func.auth_ae_obj__user_id_change_password({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
user_id: use_user_id,
password: use_password,
log_lvl: log_lvl
});
// await core_func.auth_ae_obj__user_id_change_password({
// api_cfg: $ae_api,
// account_id: $ae_loc.account_id,
// user_id: use_user_id,
// password: use_password,
// log_lvl: log_lvl
// });
// alert('Password changed successfully.');
// $ae_sess.show__modal_change_password = false;
} catch (error) {
console.error('Error changing password:', error);
alert('Unexpected Response. Failed to change password.');
} finally {
is_changing_password = false;
}
if (ae_promises.change_password) {
console.log('Password changed successfully.');
alert('Password changed successfully.');
$ae_sess.show__modal_change_password = false;
} else {
console.error('Failed to change password.');
alert('Failed to change password. Check password length.');
}
}
</script>
<!-- It is important to keep in mind that a Person can only fully sign in if they have a linked User record. -->
<!-- *:hover:inline -->
<section
id="AE-Sign-In-Out"
class="
ae_sign_in_out
hidden-print
bg-blue-100 text-gray-900
dark:bg-blue-800 dark:text-gray-200
flex flex-col flex-wrap gap-1
items-end justify-center
w-72 max-w-72
p-1
border-2 border-gray-200
duration-300 delay-150 hover:delay-1000 hover:ease-out
transition-all
"
class:hidden={hidden}
>
<header
class:hidden={!$ae_sess.show__sign_in_out__fields}
class="ae_header w-full "
>
<h2 class="text-sm text-center font-semibold">
{#if $ae_loc?.person_id && $ae_loc?.user_id}
<!-- <button
type="button"
class="btn btn-sm variant-outline-surface hover:variant-filled-surface"
title="Show/Hide Sign-In/Out Options"
onclick={() => {
$ae_sess.show__sign_in_out__fields = !$ae_sess.show__sign_in_out__fields; // Toggle the visibility of the sign-in form
}}
> -->
{$ae_loc?.person?.full_name_override ?? $ae_loc?.person?.full_name}
<!-- <span class="text-sm text-gray-500">
{$ae_loc?.user.username ?? '-- not set --'}
</span> -->
<!-- {#if log_lvl > 1}
<span class="text-xs text-gray-500">({$ae_loc.person_id} / {$ae_loc.user_id})</span>
{/if} -->
<!-- </button> -->
{:else}
<!-- <LogIn class="mx-1" /> -->
User Sign In
{/if}
</h2>
</header>
<span
class:hidden={!$ae_sess.show__sign_in_out__fields}
class="flex flex-col gap-1 transition-all w-full"
>
{#if !$ae_loc?.person_id && !$ae_loc?.user_id}
<!-- Form for user look up based on email address -->
<form
class="
ae_sign_in_form
flex flex-col gap-1 items-end justify-center
p-1 my-1
"
onsubmit={async (e) => {
e.preventDefault();
if ($ae_sess.auth__entered_email) {
alert('Attempting to look up user by email address.');
handle_lookup_user_email({
email: $ae_sess.auth__entered_email
});
} else {
alert('Please enter an email address to look up.');
}
}}
>
<input
type="text"
class="input max-w-48"
placeholder="Email Address"
value={$ae_sess.auth__entered_email ?? ''}
oninput={(e) => $ae_sess.auth__entered_email = e.target.value}
>
<button
type="submit"
class="btn btn-sm preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500"
title="Look up user by email and send sign in email"
>
<!-- <User class="mx-1" /> -->
<!-- <Mail class="mx-1" /> -->
<MailCheck class="mx-1" />
Email Sign In
</button>
</form>
<!-- We need to get the person's linked User ID and auth_key or User username and password to sign in. -->
<form
class="
ae_sign_in_form
flex flex-col gap-1 items-end
p-1 my-1
"
onsubmit={async (e) => {
e.preventDefault();
// WARNING: Logging in as a global user does not work yet. The API needs to be updated. Currently it returns multiple user records from the v_user view if there is more than one person record linked to the user ID.
if ($ae_sess.auth__entered_user_id && $ae_sess.auth__entered_user_key) {
// Try to use the user ID and user auth key passed in the URL params for authentication
alert('Attempting to authenticate with User ID and Auth Key.');
ae_promises['user'] = await core_func.auth_ae_obj__user_id_user_auth_key({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
user_id: $ae_sess.auth__entered_user_id,
user_auth_key: $ae_sess.auth__entered_user_key,
log_lvl: 2,
}).then((user_response) => {
// console.log(`HERE:`, user_response);
if (user_response.user_id_random) {
console.log(`Successfully authenticated in with User ID and User Auth Key: ${user_response.username}`, user_response);
user_obj = user_response; // Store the user object for later use
user_id = user_obj.user_id_random; // Use the user_id_random for further API calls
// person_id = user_obj.person_id_random;
} else {
alert('Failed to authenticate: ' + user_response.error);
}
}).then((response) => {
if (!user_id) {
// If we didn't get a user_id, return early
console.error('No user_id obtained from auth_ae_obj__username_password');
return;
}
// Next we need to get the person's information. This is odd because we need to look it up based on the account_id and user_id. There should only be one account person record per user. 99% of the time the user's account ID will be the same as the person's account ID. The exception is for AE Global users (Super or Manager) who can have multiple accounts but only one person record per account.
// let params = {
// user_id_random: user_id, // The user_id_random from the above authentication
// }
// let params_json: key_val = {};
// params_json['and_qry'] = {};
// if (user_id) {
// params_json['and_qry']['user_id_random'] = user_id;
// }
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
ae_promises['person'] = core_func.load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
qry_user_id: user_id, // The user_id_random from the above authentication
// params_json: params_json,
// params: params,
log_lvl: 1,
}).then((person_response) => {
if (person_response[0].person_id_random) {
console.log(`Successfully loaded person for user_id_random (${user_id}):`, person_response[0]);
person_obj = person_response[0];
person_id = person_obj.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
} else {
alert('Failed to load person information: ' + person_response.error);
}
}).then(() => {
// Once all promises are resolved, we can check if we have both user_id and person_id
if (user_id && person_id) {
// Set the session information
// $ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
// $ae_loc.person = person_obj; // Store the full person object for reference
// $ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
// $ae_loc.user = user_obj; // Store the full user object for reference
console.log(`Successfully authenticated and loaded user and person records:
user_id: ${user_id},
person_id: ${person_id}
`);
// window.location.reload(); // Reload to get the new session
} else {
console.error('Failed to authenticate and load data: missing user_id or person_id');
}
});
});
// console.log('DONE???', ae_promises);
} else if ($ae_sess.auth__entered_username && $ae_sess.auth__entered_password) {
// Try to use the username/password for authentication
// alert('Attempting to authenticate with Username and Password.');
ae_promises['user'] = await core_func.auth_ae_obj__username_password({
api_cfg: $ae_api,
account_id: $ae_loc.account_id,
// null_account_id: false, // Set to true to allow to authenticate as global user (Super or Manager)
username: $ae_sess.auth__entered_username,
password: $ae_sess.auth__entered_password,
log_lvl: 1,
}).then((user_response) => {
if (user_response.user_id_random) {
console.log(`Successfully authenticated in with Username (${user_response.username}) and Password:`, user_response);
user_obj = user_response; // Store the user object for later use
user_id = user_obj.user_id_random; // Use the user_id_random for further API calls
// person_id = user_obj.person_id_random;
} else {
alert('Failed to authenticate: ' + user_response.error);
}
}).then((response) => {
if (!user_id) {
// If we didn't get a user_id, return early
console.error('No user_id obtained from auth_ae_obj__username_password');
return;
}
// Next we need to get the person's information. This is odd because we need to look it up based on the account_id and user_id. There should only be one account person record per user. 99% of the time the user's account ID will be the same as the person's account ID. The exception is for AE Global users (Super or Manager) who can have multiple accounts but only one person record per account.
// let params = {
// user_id_random: user_id, // The user_id_random from the above authentication
// }
// let params_json: key_val = {};
// params_json['and_qry'] = {};
// if (user_id) {
// params_json['and_qry']['user_id_random'] = user_id;
// }
// WARNING: This function returns a list. We only want the first one. There should be no more than 1 record returned.
ae_promises['person'] = core_func.load_ae_obj_li__person({
api_cfg: $ae_api,
for_obj_type: 'account',
for_obj_id: $ae_loc.account_id,
qry_user_id: user_id, // The user_id_random from the above authentication
// params_json: params_json,
// params: params,
log_lvl: 1,
}).then((person_response) => {
if (person_response[0].person_id_random) {
console.log(`Successfully loaded person for user_id_random (${user_id}):`, person_response[0]);
person_obj = person_response[0];
person_id = person_obj.person_id_random;
trigger = true; // Set trigger to true to indicate we can now sign in
} else {
alert('Failed to load person information: ' + person_response.error);
}
}).then(() => {
// Once all promises are resolved, we can check if we have both user_id and person_id
if (user_id && person_id) {
// Set the session information
// $ae_loc.person_id = person_id; // Set the person_id in the ae_loc store
// $ae_loc.person = person_obj; // Store the full person object for reference
// $ae_loc.user_id = user_id; // Set the user_id in the ae_loc store
// $ae_loc.user = user_obj; // Store the full user object for reference
console.log(`Successfully authenticated and loaded user and person records:
user_id: ${user_id},
person_id: ${person_id}
`);
// window.location.reload(); // Reload to get the new session
} else {
console.error('Failed to authenticate and load data: missing user_id or person_id');
}
});
});
// console.log('DONE???', ae_promises);
} else {
alert('Please enter either a User ID and Auth Key or Username and Password.');
return false; // Prevent form submission if no credentials are provided
}
}}
>
{#if $ae_sess.auth__entered_user_id}
<input
type="text"
class="input max-w-36"
placeholder="User ID"
value={$ae_sess.auth__entered_user_id ?? ''}
oninput={(e) => $ae_sess.auth__entered_user_id = e.target.value}
>
<input
type="text"
class="input max-w-36"
placeholder="Auth Key"
value={$ae_sess.auth__entered_user_key ?? ''}
oninput={(e) => $ae_sess.auth__entered_user_key = e.target.value}
>
{:else}
<input
type="text"
class="input max-w-48"
placeholder="Username"
value={$ae_sess.auth__entered_username ?? ''}
oninput={(e) => $ae_sess.auth__entered_username = e.target.value}
>
<input
type="password"
class="input max-w-48"
placeholder="Password"
value={$ae_sess.auth__entered_password ?? ''}
oninput={(e) => $ae_sess.auth__entered_password = e.target.value}
>
{/if}
<button
type="submit"
class="btn btn-sm preset-tonal-secondary border border-secondary-500 hover:preset-filled-secondary-500"
title="Sign in with username and password"
>
<!-- <LogIn class="mx-1" /> -->
<UserCheck class="mx-1" />
{#if $ae_sess.auth__entered_user_id}
User ID
{:else}
Username
{/if}
Sign In
</button>
</form>
{:else}
<div class="flex flex-col gap-1 items-center justify-center">
<span class="text-sm text-gray-500">
{$ae_loc?.user?.username ?? '-- not set --'}
</span>
<!-- Display change password option if the user is signed in and in Edit Mode -->
{#if $ae_loc.edit_mode}
<button
type="button"
class="btn btn-sm preset-tonal-warning border border-warning-500 hover:preset-filled-warning-500"
title="Change Password"
onclick={() => {
$ae_sess.show__modal_change_password = true;
}}
>
<LockKeyhole class="mx-1" />
<span class="hidden sm:inline">
Change Password
</span>
</button>
{/if}
<!-- Display sign out option if the user is signed in -->
<button
type="button"
class="btn btn-sm preset-tonal-warning border border-warning-500 hover:preset-filled-warning-500"
title="Sign Out"
onclick={async () => {
if (confirm('Are you sure you want to sign out?')) {
sign_out();
}
}}
>
<LogOut class="mx-1" />
<span class="hidden sm:inline">
Sign Out
</span>
</button>
</div>
{/if}
</span>
<div
class="flex flex-row gap-1 items-center justify-between w-full transition-all"
>
<span>
{#if $ae_loc?.person_id && $ae_loc?.user_id}
<span class="fas fa-user mx-1 text-gray-500"></span>
{$ae_loc?.person?.full_name_override ?? $ae_loc?.person?.full_name}
{:else}
<!-- <span class="fas fa-user-x mx-1 text-gray-500"></span> -->
<ShieldUser class="inline-block text-gray-500" />
User sign in:
{/if}
</span>
<button
type="button"
class="
btn btn-sm text-xs
variant-outline-warning hover:preset-tonal-warning
border border-warning-500 group
"
title="Sign In"
onclick={() => {
$ae_sess.show__sign_in_out__fields = !$ae_sess.show__sign_in_out__fields; // Toggle the visibility of the sign-in form
if (!$ae_sess.show__sign_in_out__fields) {
$ae_sess.app_cfg.show_element__passcode_input = true;
}
}}
>
{#if $ae_loc?.person_id && $ae_loc?.user_id}
{#if $ae_sess.show__sign_in_out__fields}
<CircleX class="m-1" />
<span
class="
cfg_text
hidden
group-hover:inline
"
>
Hide Sign-In
</span>
{:else}
<User class="mx-1 inline-block text-gray-500" />
{$ae_loc?.person?.full_name_override ?? $ae_loc?.person?.full_name}
{/if}
{:else}
{#if $ae_sess.show__sign_in_out__fields}
<CircleX class="mx-1" />
<span
class="
cfg_text
hidden
group-hover:inline
"
>
Hide Sign-In
</span>
{:else}
<!-- <LockKeyhole size="1.25em" class="mx-1 inline-block text-gray-500" /> -->
<UserLock size="1.25em" class="m-1 inline-block text-gray-500" />
<!-- <User class="mx-1 inline-block text-gray-500" /> -->
<span
class="
cfg_text
hidden
group-hover:inline
"
>
User Sign-In
</span>
{/if}
{/if}
</button>
</div>
</section>
<!-- Change Password Modal -->
<!-- They should be able to see their password (text) -->
<!-- autocomplete="off" -->
{#if $ae_sess.show__modal_change_password}
<Modal
title="Change Password"
bind:open={$ae_sess.show__modal_change_password}
autoclose={false}
placement="top-center"
size="md"
class="top-center bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-200 rounded-lg border-gray-200 dark:border-gray-700 divide-gray-200 dark:divide-gray-700 shadow-md relative mx-auto w-full divide-y"
>
<div class="modal-box flex flex-col gap-2 items-center justify-center">
<!-- If the user is a global Manger or Super then they can change the password for any user. Otherwise, they can only change their own password. Show email address field for a quick lookup to get the user.id. -->
<div class="flex flex-col flex-wrap gap-2">
<span class="text-sm text-gray-500">
Your Username: {$ae_loc?.user?.username ?? '-- not set --'}
</span>
<span class="text-sm text-gray-500">
Your Email: {$ae_loc?.user?.email ?? '-- not set --'}
</span>
<span class="text-sm text-gray-500">
Your User ID: {$ae_loc?.user_id ?? '-- not set --'}
</span>
</div>
{#if ($ae_loc?.manager_access)}
<div class="flex flex-row flex-wrap gap-2">
<input
type="text"
name="user_email"
autocomplete="off"
placeholder="User's email address"
bind:value={user_email}
class="input input-bordered w-48 max-w-full"
/>
</div>
{/if}
<!-- Allow for new password and confirm password fields. -->
<div class="flex flex-row flex-wrap gap-2 items-center justify-center">
<span class="flex flex-row flex-wrap gap-1">
<input
required
type={show_password_text}
autocomplete="new-password"
name="new_password"
placeholder="New Password"
bind:value={new_password}
class="input input-bordered w-48 max-w-full required:border-red-500"
/>
<input
required
type={show_password_text}
autocomplete="off"
name="confirm_password"
placeholder="Confirm Password"
bind:value={confirm_password}
class="input input-bordered w-48 max-w-full required:border-red-500"
/>
</span>
<!-- Show/Hide Password Text -->
<button
type="button"
class="btn btn-icon preset-tonal-secondary"
onclick={() => {
// const inputs = document.querySelectorAll('.modal-box input[type="password"]');
// inputs.forEach((input) => {
// input.type = input.type === 'password' ? 'text' : 'password';
// });
if (show_password_text === 'password') {
show_password_text = 'text';
} else {
show_password_text = 'password';
}
}}
title="Show/Hide Password Text"
>
{#if show_password_text === 'password'}
<EyeOff class="mx-1"/>
{:else}
<Eye class="mx-1"/>
{/if}
<!-- <Eye class="mx-1"/> -->
<!-- <span class="hidden sm:inline">
Show/Hide
</span> -->
</button>
<div>
<span class="text-sm text-warning-800 dark:text-gray-200">
Password must be at <strong>least 10 characters</strong> long. It is recommend that you use a pass phrase (multiple words as your password) or a complex password (uppercase letter, lowercase letters, numbers, and special characters).
</span>
</div>
</div>
<div class="flex flex-row flex-wrap gap-2">
<button
type="button"
class="btn preset-filled-warning-500"
onclick={handle_change_password}
disabled={is_changing_password}
>
<Key class="mx-1" />
Change Password
</button>
<button
type="button"
class="btn preset-tonal-secondary"
onclick={() => ($ae_sess.show__modal_change_password = false)}
>
<CircleX class="mx-1" />
Cancel
</button>
</div>
</div>
</Modal>
{/if}
<style lang="scss">
</style>

View File

@@ -0,0 +1,606 @@
<script lang="ts">
// *** Import Svelte specific
// import { tick } from 'svelte';
// import { goto, invalidateAll } from '$app/navigation';
// *** Import other supporting libraries
import {
// ArrowBigRight,
// Bug,
CircleX,
// Eye, EyeOff,
// Key,
// LogIn, LogOut, LockKeyhole,
// Mail, MailCheck,
Menu,
// RefreshCw, RefreshCcwDot,
ShieldEllipsis, ShieldMinus, ShieldPlus, ShieldUser,
// ToggleLeft, ToggleRight,
User, UserCheck,
X
} from '@lucide/svelte';
// *** Import Aether specific variables and functions
// import { ae_util } from '$lib/ae_utils/ae_utils';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
import Element_access_type from '$lib/e_app_access_type.svelte';
import Element_app_cfg from '$lib/e_app_cfg.svelte';
import Element_sign_in_out from '$lib/e_app_sign_in_out.svelte';
import Element_theme from '$lib/e_app_theme.svelte';
// *** Setup Svelte properties
interface Props {
log_lvl?: number;
data: any;
hide?: null|boolean;
expand?: boolean;
}
let {
log_lvl = $bindable(0),
data = null,
hide = $bindable(false),
expand = $bindable(false),
}: Props = $props();
let trigger_clear_access: null|boolean = $state(null);
</script>
<!-- App System Menu -->
<!-- min-h-full
max-h-max
min-w-full
max-w-max -->
<!-- <section
class="
ae_app__menu
hidden-print
flex flex-col
items-end justify-center
gap-1
absolute right-0 bottom-16
hover:bottom-6
bg-white dark:bg-gray-800
border border-transparent
hover:border hover:border-gray-200 hover:dark:border-gray-700
rounded-lg
p-2
*:hover:inline
opacity-40
hover:opacity-100
duration-50 delay-1000 hover:delay-100 hover:ease-out
transition hover:transition-all
z-10 hover:z-20
"
class:hidden={!expand_btn}
> -->
<!-- && !expand -->
<!-- !$ae_loc
?.sys_menu?.expand_btn -->
<!-- bg-blue-100/60 dark:bg-blue-800/50 -->
<!-- class:hover:border-transparent={!expand} -->
<!-- mx-1 my-2 -->
<!-- We need to be able to hide the menu button in certain situations. Mainly iframes. -->
<section
class="
ae_app__sys_menu
hidden-print
z-50
absolute bottom-6 right-0
opacity-90
hover:opacity-100
w-min
max-w-md
flex flex-col-reverse gap-1
items-end justify-center
text-sm sm:text-sm md:text-md lg:text-md xl:text-md 2xl:text-lg
bg-blue-200/90
border-2 rounded-lg
border-blue-300/20 dark:border-blue-700/20
hover:border-blue-500/20 hover:dark:border-blue-500/20
transition-all
delay-500 hover:delay-200
duration-500 hover:duration-200
ease-in-out
"
class:top-0={expand && 1 == 3}
class:opacity-100={expand}
class:w-full={expand}
class:hidden={hide}
class:border-transparent={!expand}
class:bg-transparent={!expand}
>
<!-- class:hidden={!expand} -->
<!-- class:preset-filled-warning-100-900={expand} -->
<div
class:opacity-50={!expand}
class:opacity-100={expand}
class:light:bg-blue-500={expand}
class:dark:bg-blue-500={expand}
class="
hidden-print
flex flex-col items-end justify-end
gap-1
overflow-y-auto
p-1
w-full
light:bg-blue-200/10
dark:bg-blue-800/10
hover:opacity-100
relative
transition-all
delay-1000 hover:delay-500
duration-200 hover:duration-200
ease-in-out
"
title="
ID: {$ae_loc?.person_id ?? '-- not set --'} / {$ae_loc?.user_id ?? '-- not set --'}
Name: {$ae_loc?.person?.full_name ?? '-- not set --'}
Username: {$ae_loc?.user?.username ?? '-- not set --'}
Email: {$ae_loc?.user?.email ?? '-- not set --'}
Access Type: {$ae_loc?.access_type ?? '-- not set --'}
"
>
{#if $ae_loc?.person_id}
<div
class="flex flex-row gap-1 items-center justify-end transition-all w-full group"
>
<User size="1em" class="mx-1 inline-block text-gray-500" />
<span
class:hidden={!expand}
class="group-hover:inline-block"
>
{$ae_loc?.person?.informal_name ?? $ae_loc?.person?.given_name}
</span>
</div>
{/if}
{#if $ae_loc?.user_id}
<!-- This is currently not set to show if not expanded. Saving space. -->
<div
class:hidden={!expand}
class="flex flex-row gap-1 items-center justify-end transition-all w-full group"
>
<ShieldUser size="1em" class="mx-1 inline-block text-gray-500" />
<span
class:hidden={!expand}
class="group-hover:inline-block"
>
{$ae_loc?.user?.username ?? '-- not set --'}
</span>
</div>
{/if}
<div class="flex flex-row gap-1 items-center justify-end transition-all w-full group">
{#if $ae_loc.access_type && $ae_loc.access_type != 'anonymous'}
<span
class="flex flex-row-reverse gap-1 group text-base"
title={`Current access type/level: ${$ae_loc.access_type}`}
>
<!-- <span class="fas fa-unlock mx-1"></span> -->
<!-- <ShieldPlus class="inline-block" /> -->
{#if $ae_loc.access_type == 'super'}
<span class="fas fa-hat-wizard m-1"></span>
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Super</span>
{:else if $ae_loc.access_type == 'manager'}
<span class="fas fa-user-shield m-1"></span>
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Manager</span>
{:else if $ae_loc.access_type == 'administrator'}
<span class="fas fa-user-ninja m-1"></span>
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Administrator</span>
{:else if $ae_loc.access_type == 'trusted'}
<span class="fas fa-user-check m-1"></span>
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Trusted Access</span>
{:else if $ae_loc.access_type == 'public'}
Public
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Access</span>
{:else if $ae_loc.access_type == 'authenticated'}
Authenticated
<span
class:hidden={!expand}
class="hidden group-hover:inline-block"
>Access</span>
{:else if $ae_loc.access_type == 'anonymous'}
Anonymous Access
{:else}
Unknown Access
{/if}
</span>
{#if $ae_loc?.user_access_type && $ae_loc?.access_type == $ae_loc?.user_access_type}
<button
type="button"
onclick={() => {
// handle_clear_access();
// trigger_clear_access = true;
// $ae_loc.app_cfg.show_element__passcode_input = !$ae_loc.app_cfg.show_element__passcode_input;
if (!expand) {
expand = true;
$ae_sess.sys_menu.expand = true;
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
// $ae_loc.sys_menu.expand_btn = false;
// $ae_loc.app_cfg.show_element__access_type = true;
// $ae_loc.app_cfg.show_element__passcode_input = true;
} else {
// expand = true;
// $ae_loc.sys_menu.expand = false;
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
// $ae_loc.sys_menu.expand_btn = true;
}
}}
class="btn btn-sm text-xs variant-outline-surface hover:variant-ghost-warning transition-all group"
title={`Current user access level: "${$ae_loc.user_access_type}". Click use passcode for a different access level.`}
>
<!-- <span class="fas fa-lock mx-1"></span> -->
<!-- <ShieldMinus /> -->
<ShieldEllipsis size="2em" class="inline-block" />
<span class="hidden group-hover:inline-block">Passcode?</span>
</button>
{:else}
<button
type="button"
onclick={() => {
trigger_clear_access = true;
if (expand) {
$ae_loc.sys_menu.hide_access_type = false;
// $ae_loc.sys_menu.expand_access_type = false;
expand = false;
// $ae_loc.sys_menu.expand = true;
// $ae_loc.sys_menu.expand_btn = false;
$ae_loc.app_cfg.show_element__access_type = true;
$ae_sess.app_cfg.show_element__passcode_input = true;
// await tick();
// console.log('Layout button click: Focus on passcode input!');
// /** @type {HTMLElement | null} */
// const to_focus = document.getElementById('access_passcode_input');
// to_focus?.focus();
} else {
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
// $ae_loc.sys_menu.expand = false;
// $ae_loc.sys_menu.expand_btn = true;
}
}}
class="
btn btn-sm text-xs
flex-row-reverse
variant-outline-surface hover:variant-ghost-warning
transition-all group
"
title={`Current access level: "${$ae_loc.access_type}". Click to clear the temporary access level.`}
>
<!-- <span class="fas fa-lock mx-1"></span> Lock? -->
<ShieldMinus class="inline-block" />
<span class="hidden group-hover:inline-block">
Clear?
</span>
</button>
{/if}
{:else}
<button
type="button"
onclick={async () => {
expand = true;
$ae_sess.sys_menu.expand = true;
$ae_loc.sys_menu.hide_access_type = false;
$ae_loc.sys_menu.expand_access_type = true;
$ae_sess.app_cfg.show_element__passcode_input = true;
$ae_sess.sys_menu.focus_passcode_input = true;
// $ae_loc.sys_menu.expand = true;
// $ae_loc.sys_menu.expand_btn = false;
// $ae_loc.app_cfg.show_element__access_type = true;
// $ae_loc.app_cfg.show_element__passcode_input = true;
// await tick();
console.log('Layout button click: Focus on passcode input!');
/** @type {HTMLElement | null} */
const to_focus = document.getElementById('access_passcode_input');
to_focus?.focus();
}}
class="
btn btn-sm text-xs
flex-row-reverse
variant-outline-surface hover:variant-ghost-success
transition-all group
"
title="Anonymous public access is currently set. You must Sign In or use a passcode to change your access level."
>
<!-- <span class="fas fa-lock mx-1 lock_icon"></span> -->
<!-- <span class="">Unlock?</span> -->
<ShieldUser class="inline-block" />
<span class="hidden group-hover:inline-block">Auth?</span>
</button>
{/if}
</div>
{#if $ae_loc.edit_mode}
<button
type="button"
onclick={() => {
$ae_loc.edit_mode = false;
// dispatch_edit_mode_changed();
}}
class="
btn btn-base text-sm
preset-tonal-warning
preset-outlined-warning-800-200
hover:preset-tonal-success
transition-all group
min-w-22 md:min-w-30 w-full max-w-fit
gap-1
"
title="Click to turn off edit mode. Edit mode is currently on."
>
<span class="fas fa-toggle-on m-1 inline-block"></span>
<span class="text-xs">Edit</span>
<span class="hidden group-hover:inline-block group-hover:text-xs">
Off
</span>
</button>
{:else if $ae_loc.authenticated_access}
<button
type="button"
onclick={() => {
$ae_loc.edit_mode = true;
// dispatch_edit_mode_changed();
}}
class="
btn btn-base text-sm
preset-tonal-surface
preset-outlined-warning-400-600
hover:preset-tonal-warning
transition-all group
min-w-22 md:min-w-30 w-full max-w-fit
gap-1
"
title="Click to torn on/enable edit mode. Edit mode is currently off/disabled."
>
<span class="fas fa-toggle-off m-1 inline-block"></span>
<span class="text-xs">Edit</span>
<span class="hidden group-hover:inline-block group-hover:text-xs">
On?
</span>
</button>
{/if}
<!-- <div> -->
<!-- class:visible={expand} -->
<!-- class:invisible={!expand} -->
<!-- class:hover:visible={true} -->
<!-- invisible -->
<button
type="button"
class:w-full={expand}
class="
btn btn-base text-sm
preset-filled-tertiary-400-600
preset-outlined-tertiary-400-600
hover:preset-filled-success active:preset-filled-success
px-1 py-1
min-w-22 md:min-w-30 w-full max-w-fit
transition-all group
"
title="Show/Hide the system menu"
onclick={async () => {
if (!expand) {
expand = true;
$ae_sess.sys_menu.expand = true;
// $ae_loc.sys_menu.expand = true;
// $ae_loc.sys_menu.expand_btn = false;
$ae_loc.app_cfg.show_element__access_type = true;
if ($ae_loc?.access_type == 'anonymous') {
$ae_sess.sys_menu.focus_passcode_input = true;
// $ae_sess.app_cfg.show_element__passcode_input = true;
} else {
// $ae_sess.app_cfg.show_element__passcode_input = false;
$ae_loc.sys_menu.expand_user = false; // Not in use yet
$ae_sess.show__sign_in_out__fields = false;
}
// $ae_loc.app_cfg.show_element__passcode_input = true;
// await tick();
// console.log('Layout button click: Focus on passcode input!');
// /** @type {HTMLElement | null} */
// const to_focus = document.getElementById('access_passcode_input');
// to_focus?.focus();
} else {
expand = false;
$ae_sess.sys_menu.expand = false;
// $ae_loc.sys_menu.expand = false;
// $ae_loc.sys_menu.expand_btn = true;
// $ae_loc.app_cfg.show_element__passcode_input = false;
}
// $ae_loc.sys_menu.expand_btn = !expand_btn;
}}
>
<!-- <span class=""> -->
{#if expand}
<CircleX class="m-1 inline-block" />
{:else}
<Menu class="m-1 inline-block" />
{/if}
<span class="text-xs hidden group-hover:inline-block">
Menu
</span>
<!-- </span> -->
</button>
<!-- </div> -->
</div>
<!-- Show menu on right side of page -->
<!-- min-h-96 -->
<!-- absolute right-0 bottom-10 -->
<!-- opacity-50 -->
<!-- hover:opacity-100 -->
<!-- bg-white dark:bg-gray-800 -->
<div
class:preset-filled-warning-200-800={expand}
class:hidden={!expand}
class="
ae_app__sys_menu
hidden-print
flex flex-col
items-end
justify-end
gap-2
min-w-48
bg-white dark:bg-gray-800
border border-gray-200 dark:border-gray-700
rounded-lg
px-1 py-2
transition-all
delay-1000 hover:delay-100
duration-100 hover:duration-200
ease-in-out
z-20 hover:z-30
"
>
<button
type="button"
class:w-full={expand}
class="
btn btn-sm
preset-filled-tertiary-400-600
preset-outlined-tertiary-400-600
hover:preset-filled-success active:preset-filled-success
px-6 py-1
transition-all group
"
title="Show/Hide the system menu"
onclick={() => {
if (!expand) {
expand = true;
$ae_sess.sys_menu.expand = true;
// $ae_loc.sys_menu.expand_btn = false;
// $ae_loc.sys_menu.expand_access_type = true;
// $ae_sess.app_cfg.show_element__passcode_input = true;
$ae_sess.sys_menu.focus_passcode_input = true;
} else {
$ae_sess.sys_menu.expand = false;
$ae_loc.sys_menu.expand_user = false; // Not in use yet
$ae_sess.show__sign_in_out__fields = false;
// $ae_loc.sys_menu.expand_btn = true;
}
// $ae_loc.sys_menu.expand_btn = !expand_btn;
// $ae_loc.sys_menu.expand = !expand;
}}
>
{#if expand}
<CircleX class="m-1 inline-block" />
<span class="hidden group-hover:inline-block">
Hide Menu
</span>
{:else}
<Menu class="m-1 inline-block" />
<span class="hidden group-hover:inline-block">
Menu
</span>
{/if}
</button>
<!-- {#if $ae_loc?.app_cfg?.show_element__cfg} -->
<span
class:hidden={!$ae_loc.edit_mode}
>
<Element_app_cfg
hide={$ae_loc.sys_menu.hide_app_cfg}
expand={$ae_loc.sys_menu.expand_app_cfg}
/>
</span>
<span
class:hidden={!$ae_loc.edit_mode}
>
<Element_theme
hide={$ae_loc.sys_menu.hide_app_cfg}
expand={$ae_loc.sys_menu.expand_app_cfg}
set_theme_mode={true}
set_theme_name={true}
/>
</span>
<!-- {/if} -->
{#if $ae_loc?.app_cfg?.show_element__sign_in_out}
<!-- hide={$ae_loc.sys_menu.hide_user} -->
<!-- expand={$ae_loc.sys_menu.expand_user} -->
<Element_sign_in_out
data={data}
hidden={$ae_loc.iframe || !$ae_loc.app_cfg?.show_element__sign_in_out}
/>
{/if}
{#if !$ae_loc?.sys_menu?.hide_access_type && !$ae_loc?.iframe}
<!-- hidden={$ae_loc?.iframe && !$ae_loc?.trusted_access && !expand} -->
<Element_access_type
bind:hide={$ae_loc.sys_menu.hide_access_type}
bind:focus_input={$ae_sess.sys_menu.focus_passcode_input}
bind:expand={$ae_loc.sys_menu.expand_access_type}
bind:show_passcode_input={$ae_sess.app_cfg.show_element__passcode_input}
bind:trigger_clear_access={trigger_clear_access}
/>
{/if}
</div>
</section>

View File

@@ -0,0 +1,258 @@
<script lang="ts">
import {
Moon, Sun
} from '@lucide/svelte';
import { ae_loc, ae_sess, ae_api, slct, slct_trigger } from '$lib/ae_stores';
interface Props {
log_lvl?: number;
hide?: null|boolean;
expand?: boolean;
set_theme_mode: any;
set_theme_name: any;
}
let {
log_lvl = $bindable(0),
hide = $bindable(false),
expand = $bindable(false),
set_theme_mode,
set_theme_name
}: Props = $props();
</script>
<!-- Change light and dark mode -->
<!--
if ($ae_loc.app_cfg.theme_mode == 'light') {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
} else if ($ae_loc.app_cfg.theme_mode == 'dark') {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
}
-->
<section
id="AE-App-Theme"
class="
ae_app_theme
hidden-print
bg-blue-100 text-gray-900
dark:bg-blue-800 dark:text-gray-200
flex flex-col flex-wrap gap-1
items-end justify-center
w-72 max-w-72
p-1
border-2 border-gray-200
duration-300 delay-150 hover:delay-1000 hover:ease-out
transition-all
"
class:hidden={hide}
>
<div
class:hidden={!expand}
class="flex flex-row flex-wrap gap-2 items-center justify-between w-full"
>
<!-- Theme Name: -->
<span class="text-sm font-semibold">
{$ae_loc.theme_name}
</span>
<select
onchange={(event) => {
// $slct_trigger = 'set_theme_name';
let new_theme_name = event.target.value;
// document.documentElement.theme = new_theme_name;
console.log(`$ae_loc?.theme_name=${$ae_loc?.theme_name}`);
// $slct_trigger = null;
// Update the body attribute named "data-theme" to the current theme name.
// document.body.setAttribute('data-theme', new_theme_name);
// document.body.setAttribute('data-theme', $ae_loc?.theme_name);
// NEW for Tailwind v4: Update the html attribute named "data-theme" to the current theme name.
document.documentElement.setAttribute('data-theme', new_theme_name);
// if ($ae_loc.theme_mode == 'light') {
// document.documentElement.classList.remove('dark');
// document.documentElement.classList.add('light');
// } else if ($ae_loc.theme_mode == 'dark') {
// document.documentElement.classList.remove('light');
// document.documentElement.classList.add('dark');
// }
}}
bind:value={$ae_loc.theme_name}
class="select w-32"
title="Theme name"
>
<option value="">-- None --</option>
<option value="cerberus">Cerberus</option>
<option value="concord">Concord</option>
<option value="crimson">Crimson</option>
<option value="hamlindigo">Hamlindigo</option>
<option value="modern">Modern</option>
<option value="nouveau">Nouveau</option>
<option value="rocket">Rocket</option>
<option value="terminus">Terminus</option>
<option value="vintage">Vintage</option>
<option value="wintry">Wintry</option>
<!-- <option value="ae_c_osit">OSIT</option> -->
<option value="AE_OSIT_default">OSIT</option>
<option value="AE_c_IDAA_light">IDAA - light</option>
<option value="AE_c_LCI">LCI</option>
</select>
</div>
<div
class="flex flex-row flex-wrap gap-2 items-center w-full"
class:justify-between={expand}
class:justify-end={!expand}
>
{#if expand}
<!-- Hide theme options -->
<button
class="
btn btn-sm text-xs
preset-tonal-secondary hover:preset-filled-secondary-500
transition-all group
"
onclick={() => {
expand = !expand;
}}
title="Hide Theme Options"
>
<!-- <span class="fas fa-compress-alt"></span> -->
<span class="fas fa-compress-arrows-alt m-1"></span>
<span
class="
hidden
group-hover:inline-block
text-xs
"
>
Hide Theme Options
</span>
</button>
<button
class="
btn btn-sm text-xs
preset-tonal-secondary hover:preset-filled-secondary-500
transition-all group
"
onclick={() => {
if ($ae_loc.theme_mode == 'light') {
$ae_loc.theme_mode = 'dark';
} else if ($ae_loc.theme_mode == 'dark') {
$ae_loc.theme_mode = 'light';
}
if ($ae_loc.theme_mode == 'light') {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
} else if ($ae_loc.theme_mode == 'dark') {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
}
}}
title="Change light and dark mode"
>
<!-- <span class="fas fa-adjust"></span> -->
{#if $ae_loc.theme_mode == 'light'}
<Sun class="m-1" />
<span class="hidden md:inline-block group-hover:inline-block">Light Mode</span>
{:else if $ae_loc.theme_mode == 'dark'}
<Moon class="m-1" />
<span class="hidden group-hover:inline-block">Dark Mode</span>
{/if}
<span
class="
hidden
group-hover:inline-block
text-xs
"
>
Change
</span>
</button>
{:else}
<button
class="btn btn-sm preset-tonal-secondary hover:preset-filled-secondary-500 group"
onclick={() => {
if ($ae_loc.theme_mode == 'light') {
$ae_loc.theme_mode = 'dark';
} else if ($ae_loc.theme_mode == 'dark') {
$ae_loc.theme_mode = 'light';
}
if ($ae_loc.theme_mode == 'light') {
document.documentElement.classList.remove('dark');
document.documentElement.classList.add('light');
} else if ($ae_loc.theme_mode == 'dark') {
document.documentElement.classList.remove('light');
document.documentElement.classList.add('dark');
}
expand = !expand;
}}
title="Change light and dark mode"
>
{#if $ae_loc.theme_mode == 'light'}
<span class="inline-block" title="Light Mode">
<Sun />
</span>
<span class="hidden group-hover:inline-block">Light Mode</span>
{:else if $ae_loc.theme_mode == 'dark'}
<span class="inline-block" title="Dark Mode">
<Moon />
</span>
<span class="hidden group-hover:inline-block">Dark Mode</span>
{/if}
</button>
{/if}
</div>
<!-- <section class="space-y-2">
<h2 class="strong">Theme:</h2> -->
<!-- Light/Dark Theme: -->
<!-- <div>
<RadioGroup
active="variant-glass-success"
hover="hover:variant-ringed-surface"
>
<RadioItem
onchange={() => {
$slct_trigger = 'set_theme_mode';
}}
bind:group={$ae_loc.theme_mode}
name="theme_light"
value={'light'}
>
Light
</RadioItem>
<RadioItem
onchange={() => {
$slct_trigger = 'set_theme_mode';
}}
bind:group={$ae_loc.theme_mode}
name="theme_dark"
value={'dark'}
>
Dark
</RadioItem>
</RadioGroup>
</div> -->
</section>