Enhance AboutModal and AppVersionDisplay components

- Added functionality to copy the app version to clipboard with user feedback via toasts.
- Improved layout and styling of the AboutModal for better user experience.
- Updated AppVersionDisplay to support different display variants.
- Added new translations for the "Copy version" feature in the localization files.
This commit is contained in:
Sean Morley
2026-06-26 21:34:11 -04:00
parent 8b100b26af
commit 7063eb77bf
3 changed files with 262 additions and 180 deletions

View File

@@ -2,13 +2,30 @@
import { createEventDispatcher } from 'svelte';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
import { copyrightYear, versionChangelog } from '$lib/config';
import { copyrightYear, versionChangelog, appVersion } from '$lib/config';
import { copyToClipboard } from '$lib/index';
import { addToast } from '$lib/toasts';
import AppVersionDisplay from '$lib/components/shared/AppVersionDisplay.svelte';
import CloseIcon from '~icons/mdi/close';
import AccountIcon from '~icons/mdi/account-circle';
import TagIcon from '~icons/mdi/tag-outline';
import InformationIcon from '~icons/mdi/information-outline';
import ScaleBalanceIcon from '~icons/mdi/scale-balance';
import GitHubIcon from '~icons/mdi/github';
import HeartIcon from '~icons/mdi/heart';
import BookOpenIcon from '~icons/mdi/book-open-page-variant';
import DiscordIcon from '~icons/mdi/discord';
import OpenInNewIcon from '~icons/mdi/open-in-new';
import ContentCopyIcon from '~icons/mdi/content-copy';
import CheckIcon from '~icons/mdi/check';
const dispatch = createEventDispatcher();
let modal: HTMLDialogElement;
let integrations: Record<string, boolean> | null = null;
let versionCopied = false;
let copyResetTimeout: ReturnType<typeof setTimeout> | undefined;
onMount(async () => {
modal = document.getElementById('about_modal') as HTMLDialogElement;
@@ -21,6 +38,10 @@
} else {
integrations = null;
}
return () => {
if (copyResetTimeout) clearTimeout(copyResetTimeout);
};
});
function close() {
@@ -32,215 +53,271 @@
dispatch('close');
}
}
function handleBackdropClick(event: MouseEvent) {
if (event.target === modal) {
close();
}
}
async function copyVersion() {
try {
await copyToClipboard(appVersion);
versionCopied = true;
addToast('success', $t('adventures.copied_to_clipboard'));
if (copyResetTimeout) clearTimeout(copyResetTimeout);
copyResetTimeout = setTimeout(() => {
versionCopied = false;
}, 2000);
} catch {
addToast('error', $t('adventures.copy_failed'));
}
}
</script>
<dialog id="about_modal" class="modal backdrop-blur-sm">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<dialog id="about_modal" class="modal backdrop-blur-sm" on:click={handleBackdropClick}>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<!-- svelte-ignore a11y-no-noninteractive-element-interactions -->
<div
class="modal-box w-11/12 max-w-2xl bg-gradient-to-br from-base-100 via-base-100 to-base-200 border border-base-300 shadow-2xl"
class="modal-box w-11/12 max-w-2xl p-0 bg-base-100 border border-base-300 shadow-2xl max-h-[90vh] flex flex-col"
role="dialog"
on:keydown={handleKeydown}
tabindex="0"
>
<!-- Header -->
<div class="flex items-center justify-between mb-6">
<div class="flex items-center gap-3">
<div class="p-2 bg-primary/10 rounded-lg">
<img src="/favicon.png" alt="AdventureLog" class="w-12 h-12" />
</div>
<div>
<h1 class="text-2xl font-bold text-primary">
{$t('about.about')} AdventureLog
</h1>
</div>
</div>
<!-- Hero -->
<div
class="relative bg-gradient-to-r from-primary via-primary/90 to-secondary/80 text-primary-content px-6 pt-6 pb-5 shrink-0"
>
<button
type="button"
class="btn btn-ghost btn-sm btn-square"
class="btn btn-ghost btn-sm btn-circle absolute top-4 right-4 text-primary-content hover:bg-base-100/20"
aria-label={$t('about.close')}
title={$t('about.close')}
on:click={close}
>
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
<CloseIcon class="w-5 h-5" />
</button>
<div class="flex items-start gap-4 pr-10">
<div
class="h-14 w-14 rounded-2xl bg-base-100/20 backdrop-blur flex items-center justify-center shadow-inner shrink-0"
>
<img src="/favicon.png" alt="AdventureLog" class="w-9 h-9" />
</div>
<div class="min-w-0">
<p class="text-xs uppercase tracking-widest font-semibold opacity-75">
{$t('about.about')}
</p>
<h1 class="text-2xl font-bold leading-tight">AdventureLog</h1>
</div>
</div>
</div>
<!-- Content -->
<div class="space-y-4">
<!-- Version & Developer Info -->
<div class="card bg-base-200/30 border border-base-300">
<div class="card-body p-4">
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<div class="text-sm text-base-content/60">{$t('about.version')}</div>
<AppVersionDisplay />
<a
href={versionChangelog}
target="_blank"
rel="noopener noreferrer"
class="text-sm link link-primary"
>
{$t('about.view_changelog')}
</a>
</div>
<div>
<div class="text-sm text-base-content/60">{$t('about.developer')}</div>
<a
href="https://seanmorley.com"
target="_blank"
rel="noopener noreferrer"
class="text-lg font-semibold link link-primary"
>
Sean Morley
</a>
<div class="text-sm text-base-content/60">{$t('about.message')}</div>
</div>
<!-- Body -->
<div class="p-6 space-y-5 overflow-y-auto flex-1">
<!-- Version -->
<div class="rounded-2xl border border-base-300/70 bg-base-200/40 p-4 space-y-3">
<div class="flex items-center gap-2 text-primary">
<TagIcon class="w-5 h-5 shrink-0" />
<h2 class="font-semibold text-sm">{$t('about.version')}</h2>
</div>
<AppVersionDisplay />
<div
class="flex items-center gap-2 rounded-xl border border-base-300/80 bg-base-100/80 px-3 py-2"
>
<code class="flex-1 min-w-0 truncate font-mono text-sm text-base-content/80"
>{appVersion}</code
>
<button
type="button"
class="btn btn-ghost btn-xs btn-square shrink-0 {versionCopied
? 'text-success'
: 'text-base-content/60'}"
aria-label={$t('about.copy_version')}
title={$t('about.copy_version')}
on:click={copyVersion}
>
{#if versionCopied}
<CheckIcon class="w-4 h-4" />
{:else}
<ContentCopyIcon class="w-4 h-4" />
{/if}
</button>
</div>
<a
href={versionChangelog}
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 text-sm link link-primary font-medium"
>
{$t('about.view_changelog')}
<OpenInNewIcon class="w-3.5 h-3.5" />
</a>
</div>
<!-- Developer -->
<div
class="flex flex-col sm:flex-row sm:items-center gap-4 rounded-2xl border border-base-300/70 bg-base-200/40 p-4"
>
<div class="flex items-center gap-3 min-w-0">
<div
class="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center shrink-0"
>
<AccountIcon class="w-5 h-5 text-primary" />
</div>
</div>
</div>
<!-- Map Services -->
<div class="card bg-base-200/30 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold text-primary mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
{$t('about.attributions')}
</h3>
{#if integrations && integrations?.google_maps}
<div class="flex items-center gap-2">
<span class="text-sm text-base-content/60">{$t('about.nominatim_1')}</span>
<a
href="https://developers.google.com/maps/terms"
target="_blank"
rel="noopener noreferrer"
class="link link-primary font-semibold"
>
Google Maps Platform
</a>
</div>
{:else if integrations && !integrations?.google_maps}
<div class="flex items-center gap-2">
<span class="text-sm text-base-content/60">{$t('about.nominatim_1')}</span>
<a
href="https://operations.osmfoundation.org/policies/nominatim/"
target="_blank"
rel="noopener noreferrer"
class="link link-primary font-semibold"
>
OpenStreetMap Nominatim
</a>
</div>
{:else}
<div class="text-sm text-base-content/60">{$t('about.generic_attributions')}</div>
{/if}
<p class="text-sm text-base-content/60">{$t('about.other_attributions')}</p>
</div>
</div>
<!-- Liscense info -->
<div class="card bg-base-200/30 border border-base-300">
<div class="card-body p-4">
<h3 class="font-bold text-primary mb-3 flex items-center gap-2">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
/>
</svg>
{$t('about.license_info')}
</h3>
<p class="text-sm text-base-content/60 mb-2">
© {copyrightYear}
<div class="min-w-0">
<p class="text-xs font-semibold uppercase tracking-wider text-base-content/50 mb-0.5">
{$t('about.developer')}
</p>
<a
href="https://seanmorley.com"
target="_blank"
rel="noopener noreferrer"
class="link link-primary"
class="font-semibold link link-primary truncate block"
>
Sean Morley
</a>
</p>
<p class="text-sm text-base-content/60">
{$t('about.license')}
</p>
</div>
</div>
<p class="text-sm text-base-content/65 m-0 sm:ml-auto sm:text-right sm:max-w-[14rem]">
{$t('about.message')}
</p>
</div>
<a
href="https://github.com/seanmorley15/AdventureLog/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
class="link link-primary mt-2"
>
{$t('about.view_license')}
</a>
<!-- Info grid -->
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div class="rounded-2xl border border-base-300/70 bg-base-200/40 p-4 space-y-3">
<div class="flex items-center gap-2 text-primary">
<InformationIcon class="w-5 h-5 shrink-0" />
<h2 class="font-semibold text-sm">{$t('about.attributions')}</h2>
</div>
<div class="space-y-2 text-sm text-base-content/70">
{#if integrations?.google_maps}
<p class="m-0">
{$t('about.nominatim_1')}
<a
href="https://developers.google.com/maps/terms"
target="_blank"
rel="noopener noreferrer"
class="link link-primary font-medium"
>
Google Maps Platform
</a>
</p>
{:else if integrations && !integrations.google_maps}
<p class="m-0">
{$t('about.nominatim_1')}
<a
href="https://operations.osmfoundation.org/policies/nominatim/"
target="_blank"
rel="noopener noreferrer"
class="link link-primary font-medium"
>
OpenStreetMap Nominatim
</a>
</p>
{:else}
<p class="m-0">{$t('about.generic_attributions')}</p>
{/if}
<p class="m-0 text-xs text-base-content/55">{$t('about.other_attributions')}</p>
</div>
</div>
<div class="rounded-2xl border border-base-300/70 bg-base-200/40 p-4 space-y-3">
<div class="flex items-center gap-2 text-primary">
<ScaleBalanceIcon class="w-5 h-5 shrink-0" />
<h2 class="font-semibold text-sm">{$t('about.license_info')}</h2>
</div>
<div class="space-y-2 text-sm text-base-content/70">
<p class="m-0">
© {copyrightYear}
<a
href="https://seanmorley.com"
target="_blank"
rel="noopener noreferrer"
class="link link-primary"
>
Sean Morley
</a>
</p>
<p class="m-0">{$t('about.license')}</p>
<a
href="https://github.com/seanmorley15/AdventureLog/blob/main/LICENSE"
target="_blank"
rel="noopener noreferrer"
class="inline-flex items-center gap-1 link link-primary text-sm font-medium"
>
{$t('about.view_license')}
<OpenInNewIcon class="w-3.5 h-3.5" />
</a>
</div>
</div>
</div>
<!-- Links -->
<div class="card bg-base-200/30 border border-base-300">
<div class="card-body p-4">
<div class="flex flex-wrap gap-3">
<a
href="https://github.com/seanmorley15/AdventureLog"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline btn-sm"
>
GitHub →
</a>
<a
href="https://seanmorley.com/sponsor"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline btn-sm"
>
{$t('about.sponsor')}
</a>
<!-- documentation -->
<a
href="https://adventurelog.app"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline btn-sm"
>
{$t('navbar.documentation')}
</a>
<!-- discord -->
<a
href="https://discord.gg/wRbQ9Egr8C"
target="_blank"
rel="noopener noreferrer"
class="btn btn-outline btn-sm"
>
Discord →
</a>
</div>
<!-- Community links -->
<div class="space-y-3">
<p class="text-xs font-semibold uppercase tracking-wider text-base-content/50 m-0">
{$t('navbar.support')}
</p>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-2">
<a
href="https://github.com/seanmorley15/AdventureLog"
target="_blank"
rel="noopener noreferrer"
class="group flex flex-col items-center gap-2 rounded-xl border border-base-300/70 bg-base-200/30 p-3 text-center transition-colors hover:bg-base-200 hover:border-primary/30"
>
<GitHubIcon
class="w-5 h-5 text-base-content/70 group-hover:text-primary transition-colors"
/>
<span class="text-xs font-medium">GitHub</span>
</a>
<a
href="https://seanmorley.com/sponsor"
target="_blank"
rel="noopener noreferrer"
class="group flex flex-col items-center gap-2 rounded-xl border border-base-300/70 bg-base-200/30 p-3 text-center transition-colors hover:bg-base-200 hover:border-primary/30"
>
<HeartIcon
class="w-5 h-5 text-base-content/70 group-hover:text-primary transition-colors"
/>
<span class="text-xs font-medium">{$t('about.sponsor')}</span>
</a>
<a
href="https://adventurelog.app"
target="_blank"
rel="noopener noreferrer"
class="group flex flex-col items-center gap-2 rounded-xl border border-base-300/70 bg-base-200/30 p-3 text-center transition-colors hover:bg-base-200 hover:border-primary/30"
>
<BookOpenIcon
class="w-5 h-5 text-base-content/70 group-hover:text-primary transition-colors"
/>
<span class="text-xs font-medium">{$t('navbar.documentation')}</span>
</a>
<a
href="https://discord.gg/wRbQ9Egr8C"
target="_blank"
rel="noopener noreferrer"
class="group flex flex-col items-center gap-2 rounded-xl border border-base-300/70 bg-base-200/30 p-3 text-center transition-colors hover:bg-base-200 hover:border-primary/30"
>
<DiscordIcon
class="w-5 h-5 text-base-content/70 group-hover:text-primary transition-colors"
/>
<span class="text-xs font-medium">Discord</span>
</a>
</div>
</div>
</div>
<!-- Footer -->
<div class="flex items-center justify-between mt-6 pt-4 border-t border-base-300">
<div class="text-sm text-base-content/60">
{$t('about.thank_you')}
</div>
<div
class="flex items-center justify-between gap-4 px-6 py-4 border-t border-base-300 shrink-0 bg-base-100/80"
>
<p class="text-sm text-base-content/60 m-0">{$t('about.thank_you')}</p>
<button class="btn btn-primary btn-sm" on:click={close}>
{$t('about.close')}
</button>

View File

@@ -2,17 +2,21 @@
import { appTitle, appVersion, appCodename } from '$lib/config';
export let size: 'sm' | 'md' = 'md';
export let variant: 'default' | 'inverse' = 'default';
$: titleClass = variant === 'inverse' ? 'text-primary-content' : 'text-primary';
$: versionClass = variant === 'inverse' ? 'text-primary-content/80' : 'text-base-content/70';
$: codenameClass = variant === 'inverse' ? 'text-primary-content/90' : 'text-secondary';
</script>
<p
class="inline-flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 leading-snug m-0
class="flex flex-wrap items-baseline gap-x-1.5 gap-y-0.5 leading-snug m-0 w-full
{size === 'sm' ? 'text-sm' : 'text-lg'}"
>
<span class="font-bold text-primary">{appTitle}</span>
<span class="font-semibold italic text-secondary">&ldquo;{appCodename}&rdquo;</span>
<span class="text-base-content/35 font-light select-none" aria-hidden="true">·</span>
<span class="font-bold {titleClass}">{appTitle}</span>
<span
class="font-mono text-base-content/60 tabular-nums tracking-tight
class="font-mono tabular-nums tracking-tight {versionClass}
{size === 'sm' ? 'text-xs' : 'text-sm'}">{appVersion}</span
>
<span class="font-semibold italic {codenameClass}">{appCodename}</span>
</p>

View File

@@ -41,6 +41,7 @@
"close": "Close",
"thank_you": "Thank you for using AdventureLog!",
"version": "Version",
"copy_version": "Copy version",
"view_changelog": "View Changelog",
"developer": "Developer",
"attributions": "Attributions",