mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 18:41:08 +02:00
i18n(uload): wire components + routes to namespace — 67 strings cleared
Patches ListView, DetailView, /uload root page, /uload/links,
/uload/analytics/[id], /uload/settings, /uload/tags. Locale JSONs
landed in 812f3f7fa.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c9a091e696
commit
899fccd455
6 changed files with 266 additions and 177 deletions
|
|
@ -117,30 +117,30 @@
|
|||
if (!newUrl) return;
|
||||
|
||||
if (!isValidUrl(newUrl)) {
|
||||
toast.error('Bitte eine gueltige URL eingeben (mit https://)');
|
||||
toast.error($_('uload.page.err_invalid_url_input'));
|
||||
return;
|
||||
}
|
||||
|
||||
const shortCode = newCustomCode || generateShortCode();
|
||||
|
||||
if (newCustomCode && !isValidCustomCode(newCustomCode)) {
|
||||
toast.error('Custom Code darf nur Buchstaben, Zahlen, - und _ enthalten');
|
||||
toast.error($_('uload.page.err_invalid_custom_code'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(await isShortCodeUnique(shortCode))) {
|
||||
toast.error(`Short Code "${shortCode}" ist bereits vergeben`);
|
||||
toast.error($_('uload.page.err_short_code_taken', { values: { code: shortCode } }));
|
||||
return;
|
||||
}
|
||||
|
||||
const maxClicks = newMaxClicks ? parseInt(newMaxClicks) : null;
|
||||
if (maxClicks !== null && maxClicks < 1) {
|
||||
toast.error('Max Klicks muss mindestens 1 sein');
|
||||
toast.error($_('uload.page.err_max_clicks'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (newExpiresAt && new Date(newExpiresAt) <= new Date()) {
|
||||
toast.error('Ablaufdatum muss in der Zukunft liegen');
|
||||
toast.error($_('uload.page.err_expires_past'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -165,7 +165,7 @@
|
|||
};
|
||||
await encryptRecord('links', newRow);
|
||||
await linkTable.add(newRow);
|
||||
toast.success(`Link erstellt: ${shortCode}`);
|
||||
toast.success($_('uload.page.toast_created', { values: { code: shortCode } }));
|
||||
newUrl = '';
|
||||
newTitle = '';
|
||||
newCustomCode = '';
|
||||
|
|
@ -195,18 +195,18 @@
|
|||
if (!editingLink || !editUrl) return;
|
||||
|
||||
if (!isValidUrl(editUrl)) {
|
||||
toast.error('Bitte eine gueltige URL eingeben (mit https://)');
|
||||
toast.error($_('uload.page.err_invalid_url_input'));
|
||||
return;
|
||||
}
|
||||
|
||||
const maxClicks = editMaxClicks ? parseInt(editMaxClicks) : null;
|
||||
if (maxClicks !== null && maxClicks < 1) {
|
||||
toast.error('Max Klicks muss mindestens 1 sein');
|
||||
toast.error($_('uload.page.err_max_clicks'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (editExpiresAt && new Date(editExpiresAt) <= new Date()) {
|
||||
toast.error('Ablaufdatum muss in der Zukunft liegen');
|
||||
toast.error($_('uload.page.err_expires_past'));
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -222,7 +222,7 @@
|
|||
};
|
||||
await encryptRecord('links', diff);
|
||||
await linkTable.update(editingLink.id, diff);
|
||||
toast.success('Link aktualisiert');
|
||||
toast.success($_('uload.page.toast_updated'));
|
||||
editingLink = null;
|
||||
}
|
||||
|
||||
|
|
@ -231,14 +231,15 @@
|
|||
}
|
||||
|
||||
async function deleteLink(link: Link) {
|
||||
if (!confirm(`"${link.title || link.shortCode}" wirklich loeschen?`)) return;
|
||||
const name = link.title || link.shortCode;
|
||||
if (!confirm($_('uload.page.confirm_delete', { values: { name } }))) return;
|
||||
await linkTable.delete(link.id);
|
||||
toast.success('Link geloescht');
|
||||
toast.success($_('uload.page.toast_deleted'));
|
||||
}
|
||||
|
||||
function copyShortUrl(code: string) {
|
||||
navigator.clipboard.writeText(getShortUrl(code));
|
||||
toast.success('Link kopiert!');
|
||||
toast.success($_('uload.page.toast_copied'));
|
||||
}
|
||||
|
||||
function downloadQr(code: string) {
|
||||
|
|
@ -256,7 +257,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>uLoad - Mana</title>
|
||||
<title>{$_('uload.page.title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="uload">
|
||||
|
|
@ -267,10 +268,13 @@
|
|||
<div>
|
||||
<h1 class="text-2xl font-bold">uLoad</h1>
|
||||
<p class="mt-1 text-sm opacity-60">
|
||||
{filteredLinks.length} Links
|
||||
{#if folders.length > 0}
|
||||
· {folders.length} Ordner
|
||||
{/if}
|
||||
{folders.length > 0
|
||||
? $_('uload.page.counts', {
|
||||
values: { links: filteredLinks.length, folders: folders.length },
|
||||
})
|
||||
: $_('uload.page.counts_no_folders', {
|
||||
values: { links: filteredLinks.length },
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
|
|
@ -278,13 +282,13 @@
|
|||
href="/uload/links"
|
||||
class="rounded-lg border border-border-strong px-3 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Alle Links
|
||||
{$_('uload.page.all_links')}
|
||||
</a>
|
||||
<button
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-lg bg-indigo-600 px-4 py-2 font-medium text-white shadow-lg transition-[transform,colors,box-shadow] hover:scale-105 hover:bg-indigo-700"
|
||||
>
|
||||
{showCreateForm ? '- Ausblenden' : '+ Neuer Link'}
|
||||
{showCreateForm ? $_('uload.page.hide_form') : $_('uload.page.show_form')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -296,7 +300,9 @@
|
|||
>
|
||||
<div class="grid gap-4 md:grid-cols-2">
|
||||
<div class="md:col-span-2">
|
||||
<label for="url" class="mb-1 block text-sm font-medium">URL</label>
|
||||
<label for="url" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_url_modal')}</label
|
||||
>
|
||||
<input
|
||||
id="url"
|
||||
type="url"
|
||||
|
|
@ -307,23 +313,26 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="title" class="mb-1 block text-sm font-medium">Titel (optional)</label>
|
||||
<label for="title" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_title')}</label
|
||||
>
|
||||
<input
|
||||
id="title"
|
||||
type="text"
|
||||
bind:value={newTitle}
|
||||
placeholder="Mein Link"
|
||||
placeholder={$_('uload.page.placeholder_title')}
|
||||
class={inputClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="code" class="mb-1 block text-sm font-medium">Custom Code (optional)</label
|
||||
<label for="code" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_custom_code')}</label
|
||||
>
|
||||
<input
|
||||
id="code"
|
||||
type="text"
|
||||
bind:value={newCustomCode}
|
||||
placeholder="mein-link"
|
||||
placeholder={$_('uload.page.placeholder_custom_code')}
|
||||
class={inputClass}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -337,13 +346,13 @@
|
|||
<span class="transition-transform {showAdvanced ? 'rotate-90' : ''}"
|
||||
><CaretRight size={16} /></span
|
||||
>
|
||||
Erweitert
|
||||
{$_('uload.page.section_advanced')}
|
||||
</button>
|
||||
{#if showAdvanced}
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label for="expires" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Ablaufdatum</label
|
||||
>{$_('uload.page.label_expires')}</label
|
||||
>
|
||||
<input
|
||||
id="expires"
|
||||
|
|
@ -354,25 +363,25 @@
|
|||
</div>
|
||||
<div>
|
||||
<label for="password" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Passwort</label
|
||||
>{$_('uload.page.label_password')}</label
|
||||
>
|
||||
<input
|
||||
id="password"
|
||||
type="text"
|
||||
bind:value={newPassword}
|
||||
placeholder="Optional"
|
||||
placeholder={$_('uload.page.placeholder_optional')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="maxclicks" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Max Klicks</label
|
||||
>{$_('uload.page.label_max_clicks')}</label
|
||||
>
|
||||
<input
|
||||
id="maxclicks"
|
||||
type="number"
|
||||
bind:value={newMaxClicks}
|
||||
placeholder="Unbegrenzt"
|
||||
placeholder={$_('uload.page.placeholder_unlimited')}
|
||||
min="1"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
|
|
@ -388,43 +397,43 @@
|
|||
<span class="transition-transform {showUtm ? 'rotate-90' : ''}"
|
||||
><CaretRight size={16} /></span
|
||||
>
|
||||
UTM-Parameter
|
||||
{$_('uload.page.section_utm')}
|
||||
</button>
|
||||
{#if showUtm}
|
||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label for="utm-source" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Source</label
|
||||
>{$_('uload.page.label_source')}</label
|
||||
>
|
||||
<input
|
||||
id="utm-source"
|
||||
type="text"
|
||||
bind:value={newUtmSource}
|
||||
placeholder="newsletter"
|
||||
placeholder={$_('uload.page.placeholder_source')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="utm-medium" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Medium</label
|
||||
>{$_('uload.page.label_medium')}</label
|
||||
>
|
||||
<input
|
||||
id="utm-medium"
|
||||
type="text"
|
||||
bind:value={newUtmMedium}
|
||||
placeholder="email"
|
||||
placeholder={$_('uload.page.placeholder_medium')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="utm-campaign" class="mb-1 block text-xs font-medium opacity-70"
|
||||
>Campaign</label
|
||||
>{$_('uload.page.label_campaign')}</label
|
||||
>
|
||||
<input
|
||||
id="utm-campaign"
|
||||
type="text"
|
||||
bind:value={newUtmCampaign}
|
||||
placeholder="spring-2026"
|
||||
placeholder={$_('uload.page.placeholder_campaign')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -437,7 +446,7 @@
|
|||
disabled={!newUrl}
|
||||
class="rounded-lg bg-indigo-600 px-6 py-2.5 font-medium text-white transition-colors hover:bg-indigo-700 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
Link erstellen
|
||||
{$_('uload.page.action_create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -453,18 +462,18 @@
|
|||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Links durchsuchen..."
|
||||
placeholder={$_('uload.page.placeholder_search')}
|
||||
class="w-60 rounded-lg border border-border-strong bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||
<option value="all">Alle</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="all">{$_('uload.page.option_all')}</option>
|
||||
<option value="active">{$_('uload.page.option_active')}</option>
|
||||
<option value="inactive">{$_('uload.page.option_inactive')}</option>
|
||||
</select>
|
||||
{#if folders.length > 0}
|
||||
<select bind:value={selectedFolderId} class={inputSmClass} style="max-width: 160px">
|
||||
<option value={null}>Alle Ordner</option>
|
||||
<option value={null}>{$_('uload.page.option_all_folders')}</option>
|
||||
{#each folders as folder}
|
||||
<option value={folder.id}>{folder.name}</option>
|
||||
{/each}
|
||||
|
|
@ -484,13 +493,13 @@
|
|||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<LinkIcon size={48} class="mx-auto mb-4 opacity-20" />
|
||||
<p class="text-lg font-medium opacity-60">Noch keine Links</p>
|
||||
<p class="mt-1 text-sm opacity-40">Erstelle deinen ersten gekuerzten Link!</p>
|
||||
<p class="text-lg font-medium opacity-60">{$_('uload.page.empty_title')}</p>
|
||||
<p class="mt-1 text-sm opacity-40">{$_('uload.page.empty_desc')}</p>
|
||||
<button
|
||||
onclick={() => (showCreateForm = true)}
|
||||
class="mt-4 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
+ Neuer Link
|
||||
{$_('uload.page.show_form')}
|
||||
</button>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -516,19 +525,21 @@
|
|||
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
||||
<span
|
||||
class="shrink-0 rounded bg-amber-100 px-1.5 py-0.5 text-xs text-amber-700 dark:bg-amber-900 dark:text-amber-300"
|
||||
>UTM</span
|
||||
>{$_('uload.page.badge_utm')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if link.password}
|
||||
<span
|
||||
class="shrink-0 rounded bg-red-100 px-1.5 py-0.5 text-xs text-red-700 dark:bg-red-900 dark:text-red-300"
|
||||
>Passwort</span
|
||||
>{$_('uload.page.badge_password')}</span
|
||||
>
|
||||
{/if}
|
||||
{#if link.expiresAt}
|
||||
<span
|
||||
class="shrink-0 rounded bg-orange-100 px-1.5 py-0.5 text-xs text-orange-700 dark:bg-orange-900 dark:text-orange-300"
|
||||
title="Laeuft ab: {formatDate(new Date(link.expiresAt))}">Ablauf</span
|
||||
title={$_('uload.page.badge_expires_title', {
|
||||
values: { date: formatDate(new Date(link.expiresAt)) },
|
||||
})}>{$_('uload.page.badge_expires')}</span
|
||||
>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -552,7 +563,7 @@
|
|||
<a
|
||||
href="/uload/analytics/{link.id}"
|
||||
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
|
||||
title="Analytics"
|
||||
title={$_('uload.page.action_analytics_title')}
|
||||
>
|
||||
<ChartBar size={16} />
|
||||
{link.clickCount}
|
||||
|
|
@ -560,28 +571,30 @@
|
|||
<button
|
||||
onclick={() => copyShortUrl(link.shortCode)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title="Link kopieren"
|
||||
title={$_('uload.page.action_copy_title')}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => (qrLink = link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title="QR-Code"
|
||||
title={$_('uload.page.action_qr_title')}
|
||||
>
|
||||
<QrCode size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => openEdit(link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={$_('common.edit')}
|
||||
title={$_('uload.page.action_edit_title')}
|
||||
>
|
||||
<PencilSimple size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => toggleActive(link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
||||
title={link.isActive
|
||||
? $_('uload.page.action_deactivate_title')
|
||||
: $_('uload.page.action_activate_title')}
|
||||
>
|
||||
<Lightning
|
||||
size={16}
|
||||
|
|
@ -591,7 +604,7 @@
|
|||
<button
|
||||
onclick={() => deleteLink(link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
|
||||
title="Loeschen"
|
||||
title={$_('uload.page.action_delete_title')}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
|
|
@ -620,7 +633,7 @@
|
|||
role="none"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">Link bearbeiten</h3>
|
||||
<h3 class="text-lg font-semibold">{$_('uload.page.modal_edit_title')}</h3>
|
||||
<button
|
||||
onclick={() => (editingLink = null)}
|
||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
|
|
@ -631,50 +644,57 @@
|
|||
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<label for="edit-url" class="mb-1 block text-sm font-medium">URL</label>
|
||||
<label for="edit-url" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_url_modal')}</label
|
||||
>
|
||||
<input id="edit-url" type="url" bind:value={editUrl} class={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="edit-title" class="mb-1 block text-sm font-medium">Titel</label>
|
||||
<label for="edit-title" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_title_modal')}</label
|
||||
>
|
||||
<input id="edit-title" type="text" bind:value={editTitle} class={inputClass} />
|
||||
</div>
|
||||
<div>
|
||||
<label for="edit-code" class="mb-1 block text-sm font-medium">Short Code</label>
|
||||
<label for="edit-code" class="mb-1 block text-sm font-medium"
|
||||
>{$_('uload.page.label_short_code_modal')}</label
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm opacity-50">/{editingLink.shortCode}</span>
|
||||
<span class="text-xs opacity-30">(nicht aenderbar)</span>
|
||||
<span class="text-xs opacity-30">{$_('uload.page.short_code_locked')}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
||||
<p class="mb-2 text-sm font-medium opacity-70">UTM-Parameter</p>
|
||||
<p class="mb-2 text-sm font-medium opacity-70">{$_('uload.page.section_utm')}</p>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editUtmSource}
|
||||
placeholder="Source"
|
||||
placeholder={$_('uload.page.label_source')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editUtmMedium}
|
||||
placeholder="Medium"
|
||||
placeholder={$_('uload.page.label_medium')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
bind:value={editUtmCampaign}
|
||||
placeholder="Campaign"
|
||||
placeholder={$_('uload.page.label_campaign')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
||||
<p class="mb-2 text-sm font-medium opacity-70">Erweitert</p>
|
||||
<p class="mb-2 text-sm font-medium opacity-70">{$_('uload.page.section_advanced')}</p>
|
||||
<div class="grid gap-3 md:grid-cols-3">
|
||||
<div>
|
||||
<label for="uload-expires-at" class="mb-1 block text-xs opacity-50">Ablaufdatum</label
|
||||
<label for="uload-expires-at" class="mb-1 block text-xs opacity-50"
|
||||
>{$_('uload.page.label_expires')}</label
|
||||
>
|
||||
<input
|
||||
id="uload-expires-at"
|
||||
|
|
@ -684,22 +704,26 @@
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="uload-password" class="mb-1 block text-xs opacity-50">Passwort</label>
|
||||
<label for="uload-password" class="mb-1 block text-xs opacity-50"
|
||||
>{$_('uload.page.label_password')}</label
|
||||
>
|
||||
<input
|
||||
id="uload-password"
|
||||
type="text"
|
||||
bind:value={editPassword}
|
||||
placeholder="Optional"
|
||||
placeholder={$_('uload.page.placeholder_optional')}
|
||||
class={inputSmClass}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="uload-max-clicks" class="mb-1 block text-xs opacity-50">Max Klicks</label>
|
||||
<label for="uload-max-clicks" class="mb-1 block text-xs opacity-50"
|
||||
>{$_('uload.page.label_max_clicks')}</label
|
||||
>
|
||||
<input
|
||||
id="uload-max-clicks"
|
||||
type="number"
|
||||
bind:value={editMaxClicks}
|
||||
placeholder="Unbegrenzt"
|
||||
placeholder={$_('uload.page.placeholder_unlimited')}
|
||||
min="1"
|
||||
class={inputSmClass}
|
||||
/>
|
||||
|
|
@ -713,14 +737,14 @@
|
|||
onclick={() => (editingLink = null)}
|
||||
class="rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Abbrechen
|
||||
{$_('uload.page.action_cancel')}
|
||||
</button>
|
||||
<button
|
||||
onclick={saveEdit}
|
||||
disabled={!editUrl}
|
||||
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
>
|
||||
Speichern
|
||||
{$_('uload.page.action_save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -742,7 +766,7 @@
|
|||
role="none"
|
||||
>
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h3 class="text-lg font-semibold">QR-Code</h3>
|
||||
<h3 class="text-lg font-semibold">{$_('uload.page.modal_qr_title')}</h3>
|
||||
<button
|
||||
onclick={() => (qrLink = null)}
|
||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||
|
|
@ -755,7 +779,7 @@
|
|||
<div class="rounded-lg bg-white p-4">
|
||||
<img
|
||||
src="{QR_API}/?size=200x200&data={encodeURIComponent(getShortUrl(qrLink.shortCode))}"
|
||||
alt="QR Code fuer {qrLink.shortCode}"
|
||||
alt={$_('uload.page.qr_alt', { values: { code: qrLink.shortCode } })}
|
||||
class="h-48 w-48"
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -765,13 +789,13 @@
|
|||
onclick={() => copyShortUrl(qrLink!.shortCode)}
|
||||
class="flex-1 rounded-lg border border-border-strong px-4 py-2 text-sm font-medium hover:bg-muted dark:border-border dark:hover:bg-muted"
|
||||
>
|
||||
Link kopieren
|
||||
{$_('uload.page.action_copy_link')}
|
||||
</button>
|
||||
<button
|
||||
onclick={() => downloadQr(qrLink!.shortCode)}
|
||||
class="flex-1 rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
QR herunterladen
|
||||
{$_('uload.page.action_download_qr')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { formatDate } from '$lib/i18n/format';
|
||||
import { page } from '$app/stores';
|
||||
import { onMount } from 'svelte';
|
||||
|
|
@ -71,18 +72,22 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Analytics - uLoad - Mana</title>
|
||||
<title>{$_('uload.analytics_route.title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="uload" backHref="/uload" title="Link">
|
||||
<RoutePage appId="uload" backHref="/uload" title={$_('uload.analytics_route.page_title')}>
|
||||
<div class="mx-auto max-w-4xl p-4">
|
||||
<!-- Header -->
|
||||
<div class="mb-6 flex items-center gap-4">
|
||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5" title="Zurueck">
|
||||
<a
|
||||
href="/uload"
|
||||
class="rounded-lg p-2 transition-colors hover:bg-muted/5"
|
||||
title={$_('uload.analytics_route.action_back_title')}
|
||||
>
|
||||
<CaretLeft size={20} class="text-muted-foreground" />
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold text-white">Analytics</h1>
|
||||
<h1 class="text-2xl font-bold text-white">{$_('uload.analytics_route.heading')}</h1>
|
||||
{#if link}
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
<span class="font-mono text-indigo-400">/{link.shortCode}</span>
|
||||
|
|
@ -100,35 +105,45 @@
|
|||
</div>
|
||||
{:else if !link}
|
||||
<div class="rounded-xl border border-border/10 p-12 text-center">
|
||||
<p class="text-muted-foreground">Link nicht gefunden</p>
|
||||
<p class="text-muted-foreground">{$_('uload.analytics_route.not_found')}</p>
|
||||
</div>
|
||||
{:else}
|
||||
<!-- Stats Overview -->
|
||||
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Clicks</p>
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{$_('uload.analytics_route.stat_clicks')}
|
||||
</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">
|
||||
{stats?.totalClicks ?? link.clickCount}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Unique</p>
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{$_('uload.analytics_route.stat_unique')}
|
||||
</p>
|
||||
<p class="mt-1 text-3xl font-bold text-white">
|
||||
{stats?.uniqueVisitors ?? '-'}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Status</p>
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{$_('uload.analytics_route.stat_status')}
|
||||
</p>
|
||||
<p class="mt-1 text-3xl font-bold">
|
||||
{#if link.isActive}
|
||||
<span class="text-green-400">Aktiv</span>
|
||||
<span class="text-green-400">{$_('uload.analytics_route.status_active')}</span>
|
||||
{:else}
|
||||
<span class="text-muted-foreground/70">Inaktiv</span>
|
||||
<span class="text-muted-foreground/70"
|
||||
>{$_('uload.analytics_route.status_inactive')}</span
|
||||
>
|
||||
{/if}
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">Erstellt</p>
|
||||
<p class="text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
{$_('uload.analytics_route.stat_created')}
|
||||
</p>
|
||||
<p class="mt-1 text-lg font-bold text-white">
|
||||
{formatDate(new Date(link.createdAt))}
|
||||
</p>
|
||||
|
|
@ -137,10 +152,13 @@
|
|||
|
||||
<!-- Link Details -->
|
||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Link Details</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">
|
||||
{$_('uload.analytics_route.section_details')}
|
||||
</h2>
|
||||
<div class="space-y-3">
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Ziel-URL</span>
|
||||
<span class="text-muted-foreground">{$_('uload.analytics_route.label_target_url')}</span
|
||||
>
|
||||
<a
|
||||
href={link.originalUrl}
|
||||
target="_blank"
|
||||
|
|
@ -152,31 +170,37 @@
|
|||
</div>
|
||||
{#if link.title}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Titel</span>
|
||||
<span class="text-muted-foreground">{$_('uload.analytics_route.label_title')}</span>
|
||||
<span class="text-white">{link.title}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
||||
<div class="border-t border-border/10 pt-3">
|
||||
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||
UTM-Parameter
|
||||
{$_('uload.analytics_route.label_utm_params')}
|
||||
</p>
|
||||
<div class="grid gap-2 sm:grid-cols-3">
|
||||
{#if link.utmSource}
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Source:</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.utm_source')}</span
|
||||
>
|
||||
{link.utmSource}
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmMedium}
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Medium:</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.utm_medium')}</span
|
||||
>
|
||||
{link.utmMedium}
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.utmCampaign}
|
||||
<div class="text-sm text-foreground">
|
||||
<span class="text-muted-foreground">Campaign:</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.utm_campaign')}</span
|
||||
>
|
||||
{link.utmCampaign}
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -185,20 +209,30 @@
|
|||
{/if}
|
||||
{#if link.expiresAt}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Laeuft ab</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.label_expires_at')}</span
|
||||
>
|
||||
<span class="text-white">{formatDate(new Date(link.expiresAt))}</span>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.maxClicks}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Max Klicks</span>
|
||||
<span class="text-white">{link.clickCount} / {link.maxClicks}</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.label_max_clicks')}</span
|
||||
>
|
||||
<span class="text-white"
|
||||
>{$_('uload.analytics_route.max_clicks_value', {
|
||||
values: { used: link.clickCount, max: link.maxClicks },
|
||||
})}</span
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
{#if link.password}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="text-muted-foreground">Passwortgeschuetzt</span>
|
||||
<span class="text-white">Ja</span>
|
||||
<span class="text-muted-foreground"
|
||||
>{$_('uload.analytics_route.label_password_protected')}</span
|
||||
>
|
||||
<span class="text-white">{$_('uload.analytics_route.yes')}</span>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -207,7 +241,9 @@
|
|||
<!-- Timeline -->
|
||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<div class="mb-4 flex items-center justify-between">
|
||||
<h2 class="text-lg font-semibold text-white">Clicks ueber Zeit</h2>
|
||||
<h2 class="text-lg font-semibold text-white">
|
||||
{$_('uload.analytics_route.section_timeline')}
|
||||
</h2>
|
||||
<div class="flex gap-1">
|
||||
{#each [7, 30, 90] as d}
|
||||
<button
|
||||
|
|
@ -216,7 +252,7 @@
|
|||
? 'bg-indigo-600 text-white'
|
||||
: 'bg-muted/10 text-muted-foreground hover:bg-muted/15'}"
|
||||
>
|
||||
{d}T
|
||||
{$_('uload.analytics_route.days_unit', { values: { days: d } })}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
|
|
@ -244,15 +280,17 @@
|
|||
{:else if !serverAvailable}
|
||||
<div class="py-8 text-center">
|
||||
<p class="text-sm text-muted-foreground">
|
||||
Detaillierte Analytics sind verfuegbar, wenn der uLoad-Server verbunden ist.
|
||||
{$_('uload.analytics_route.hint_no_server')}
|
||||
</p>
|
||||
<p class="mt-1 text-xs text-muted-foreground/70">
|
||||
Lokaler Click-Count: {link.clickCount}
|
||||
{$_('uload.analytics_route.hint_local_count', {
|
||||
values: { count: link.clickCount },
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
<p class="py-8 text-center text-sm text-muted-foreground">
|
||||
Noch keine Daten fuer diesen Zeitraum
|
||||
{$_('uload.analytics_route.empty_period')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
|
@ -262,13 +300,17 @@
|
|||
<div class="grid gap-6 md:grid-cols-3">
|
||||
<!-- Devices -->
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Geraete</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">
|
||||
{$_('uload.analytics_route.section_devices')}
|
||||
</h2>
|
||||
{#if devices.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each devices as d}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-foreground">{d.deviceType || 'Unbekannt'}</span>
|
||||
<span class="text-foreground"
|
||||
>{d.deviceType || $_('uload.analytics_route.unknown')}</span
|
||||
>
|
||||
<span class="font-medium text-white">
|
||||
{Math.round((d.count / totalDevices) * 100)}%
|
||||
</span>
|
||||
|
|
@ -283,38 +325,48 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('uload.analytics_route.empty_no_data')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Referrers -->
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Referrer</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">
|
||||
{$_('uload.analytics_route.section_referrers')}
|
||||
</h2>
|
||||
{#if referrers.length > 0}
|
||||
<div class="space-y-2">
|
||||
{#each referrers.slice(0, 8) as r}
|
||||
<div class="flex items-center justify-between text-sm">
|
||||
<span class="max-w-[140px] truncate text-foreground">
|
||||
{r.referer || 'Direkt'}
|
||||
{r.referer || $_('uload.analytics_route.direct')}
|
||||
</span>
|
||||
<span class="font-medium tabular-nums text-white">{r.count}</span>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('uload.analytics_route.empty_no_data')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<!-- Countries -->
|
||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">Laender</h2>
|
||||
<h2 class="mb-4 text-lg font-semibold text-white">
|
||||
{$_('uload.analytics_route.section_countries')}
|
||||
</h2>
|
||||
{#if countries.length > 0}
|
||||
<div class="space-y-3">
|
||||
{#each countries.slice(0, 8) as c}
|
||||
<div>
|
||||
<div class="mb-1 flex items-center justify-between text-sm">
|
||||
<span class="text-foreground">{c.country || 'Unbekannt'}</span>
|
||||
<span class="text-foreground"
|
||||
>{c.country || $_('uload.analytics_route.unknown')}</span
|
||||
>
|
||||
<span class="font-medium text-white">
|
||||
{Math.round((c.count / totalCountries) * 100)}%
|
||||
</span>
|
||||
|
|
@ -329,7 +381,9 @@
|
|||
{/each}
|
||||
</div>
|
||||
{:else}
|
||||
<p class="text-sm text-muted-foreground">Keine Daten</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{$_('uload.analytics_route.empty_no_data')}
|
||||
</p>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import {
|
||||
useAllLinks,
|
||||
useAllTags,
|
||||
|
|
@ -68,7 +69,7 @@
|
|||
|
||||
function copyShortUrl(code: string) {
|
||||
navigator.clipboard.writeText(getShortUrl(code));
|
||||
toast.success('Link kopiert!');
|
||||
toast.success($_('uload.links_route.toast_copied'));
|
||||
}
|
||||
|
||||
async function toggleActive(link: Link) {
|
||||
|
|
@ -76,9 +77,10 @@
|
|||
}
|
||||
|
||||
async function deleteLink(link: Link) {
|
||||
if (!confirm(`"${link.title || link.shortCode}" wirklich loeschen?`)) return;
|
||||
const name = link.title || link.shortCode;
|
||||
if (!confirm($_('uload.links_route.confirm_delete_single', { values: { name } }))) return;
|
||||
await linkTable.delete(link.id);
|
||||
toast.success('Link geloescht');
|
||||
toast.success($_('uload.links_route.toast_deleted_single'));
|
||||
}
|
||||
|
||||
// Bulk actions
|
||||
|
|
@ -101,22 +103,24 @@
|
|||
}
|
||||
|
||||
async function bulkDelete() {
|
||||
if (!confirm(`${selectedIds.size} Link(s) loeschen?`)) return;
|
||||
const count = selectedIds.size;
|
||||
if (!confirm($_('uload.links_route.confirm_bulk_delete', { values: { count } }))) return;
|
||||
for (const id of selectedIds) {
|
||||
await linkTable.delete(id);
|
||||
}
|
||||
toast.success(`${selectedIds.size} Links geloescht`);
|
||||
toast.success($_('uload.links_route.toast_bulk_deleted', { values: { count } }));
|
||||
selectedIds.clear();
|
||||
selectedIds = selectedIds;
|
||||
selectMode = false;
|
||||
}
|
||||
|
||||
async function bulkToggleActive() {
|
||||
const count = selectedIds.size;
|
||||
for (const id of selectedIds) {
|
||||
const link = filteredLinks.find((l) => l.id === id);
|
||||
if (link) await linkTable.update(id, { isActive: !link.isActive });
|
||||
}
|
||||
toast.success(`${selectedIds.size} Links aktualisiert`);
|
||||
toast.success($_('uload.links_route.toast_bulk_updated', { values: { count } }));
|
||||
selectedIds.clear();
|
||||
selectedIds = selectedIds;
|
||||
selectMode = false;
|
||||
|
|
@ -127,7 +131,7 @@
|
|||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>Alle Links - uLoad - Mana</title>
|
||||
<title>{$_('uload.links_route.title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="uload" backHref="/uload">
|
||||
|
|
@ -139,13 +143,13 @@
|
|||
<a
|
||||
href="/uload"
|
||||
class="rounded-lg p-2 hover:bg-muted dark:hover:bg-muted"
|
||||
title="Zurueck"
|
||||
title={$_('uload.links_route.action_back_title')}
|
||||
>
|
||||
<ArrowLeft size={20} />
|
||||
</a>
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">
|
||||
Alle Links
|
||||
{$_('uload.links_route.heading')}
|
||||
{#if filteredLinks.length > 0}
|
||||
<span class="ml-1 text-xl opacity-50">({filteredLinks.length})</span>
|
||||
{/if}
|
||||
|
|
@ -165,7 +169,9 @@
|
|||
? 'bg-indigo-600 text-white'
|
||||
: 'hover:bg-muted dark:border-border dark:hover:bg-muted'}"
|
||||
>
|
||||
{selectMode ? 'Fertig' : 'Auswaehlen'}
|
||||
{selectMode
|
||||
? $_('uload.links_route.action_select_done')
|
||||
: $_('uload.links_route.action_select_start')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -180,18 +186,18 @@
|
|||
<input
|
||||
type="text"
|
||||
bind:value={searchQuery}
|
||||
placeholder="Links durchsuchen..."
|
||||
placeholder={$_('uload.links_route.placeholder_search')}
|
||||
class="w-60 rounded-lg border border-border-strong bg-white py-2 pl-8 pr-3 text-sm focus:border-indigo-500 focus:outline-none dark:border-border dark:bg-muted"
|
||||
/>
|
||||
</div>
|
||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||
<option value="all">Alle</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="inactive">Inaktiv</option>
|
||||
<option value="all">{$_('uload.links_route.option_all')}</option>
|
||||
<option value="active">{$_('uload.links_route.option_active')}</option>
|
||||
<option value="inactive">{$_('uload.links_route.option_inactive')}</option>
|
||||
</select>
|
||||
{#if folders.length > 0}
|
||||
<select bind:value={selectedFolderId} class={inputSmClass} style="max-width: 160px">
|
||||
<option value={null}>Alle Ordner</option>
|
||||
<option value={null}>{$_('uload.links_route.option_all_folders')}</option>
|
||||
{#each folders as folder}
|
||||
<option value={folder.id}>{folder.name}</option>
|
||||
{/each}
|
||||
|
|
@ -211,18 +217,22 @@
|
|||
onchange={toggleSelectAll}
|
||||
class="h-4 w-4 rounded"
|
||||
/>
|
||||
<span class="text-sm font-medium">{selectedIds.size} ausgewaehlt</span>
|
||||
<span class="text-sm font-medium"
|
||||
>{$_('uload.links_route.selected_count', {
|
||||
values: { count: selectedIds.size },
|
||||
})}</span
|
||||
>
|
||||
</label>
|
||||
<div class="h-4 w-px bg-indigo-300 dark:bg-indigo-700"></div>
|
||||
<button
|
||||
onclick={bulkToggleActive}
|
||||
class="rounded px-3 py-1 text-sm font-medium hover:bg-indigo-100 dark:hover:bg-indigo-800"
|
||||
>Aktivieren/Deaktivieren</button
|
||||
>{$_('uload.links_route.action_bulk_toggle')}</button
|
||||
>
|
||||
<button
|
||||
onclick={bulkDelete}
|
||||
class="rounded px-3 py-1 text-sm font-medium text-red-600 hover:bg-red-50 dark:hover:bg-red-900/20"
|
||||
>Loeschen</button
|
||||
>{$_('uload.links_route.action_bulk_delete')}</button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
|
|
@ -239,11 +249,11 @@
|
|||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
||||
>
|
||||
<LinkIcon size={48} class="mx-auto mb-4 opacity-20" />
|
||||
<p class="text-lg font-medium opacity-60">Keine Links gefunden</p>
|
||||
<p class="text-lg font-medium opacity-60">{$_('uload.links_route.empty_title')}</p>
|
||||
{#if searchQuery || selectedStatus !== 'all' || selectedFolderId}
|
||||
<p class="mt-1 text-sm opacity-40">Versuche andere Filtereinstellungen.</p>
|
||||
<p class="mt-1 text-sm opacity-40">{$_('uload.links_route.empty_filtered')}</p>
|
||||
{:else}
|
||||
<p class="mt-1 text-sm opacity-40">Erstelle Links auf der uLoad-Hauptseite.</p>
|
||||
<p class="mt-1 text-sm opacity-40">{$_('uload.links_route.empty_root')}</p>
|
||||
{/if}
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -301,7 +311,7 @@
|
|||
<a
|
||||
href="/uload/analytics/{link.id}"
|
||||
class="flex items-center gap-1 rounded-lg px-2 py-1.5 text-sm font-medium opacity-60 transition-colors hover:bg-muted hover:opacity-100 dark:hover:bg-muted"
|
||||
title="Analytics"
|
||||
title={$_('uload.links_route.action_analytics_title')}
|
||||
>
|
||||
<ChartBar size={16} />
|
||||
{link.clickCount}
|
||||
|
|
@ -309,14 +319,16 @@
|
|||
<button
|
||||
onclick={() => copyShortUrl(link.shortCode)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title="Link kopieren"
|
||||
title={$_('uload.links_route.action_copy_title')}
|
||||
>
|
||||
<Copy size={16} />
|
||||
</button>
|
||||
<button
|
||||
onclick={() => toggleActive(link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
||||
title={link.isActive ? 'Deaktivieren' : 'Aktivieren'}
|
||||
title={link.isActive
|
||||
? $_('uload.links_route.action_deactivate_title')
|
||||
: $_('uload.links_route.action_activate_title')}
|
||||
>
|
||||
<Lightning
|
||||
size={16}
|
||||
|
|
@ -326,7 +338,7 @@
|
|||
<button
|
||||
onclick={() => deleteLink(link)}
|
||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-red-50 hover:text-red-600 group-hover:opacity-100 dark:hover:bg-red-900/20"
|
||||
title="Loeschen"
|
||||
title={$_('uload.links_route.action_delete_title')}
|
||||
>
|
||||
<Trash size={16} />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
Reached via the ⚙ button in the uLoad module; not a workbench card.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { _ } from 'svelte-i18n';
|
||||
import { Trash, DownloadSimple } from '@mana/shared-icons';
|
||||
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
||||
import { decryptRecords } from '$lib/data/crypto';
|
||||
|
|
@ -15,14 +16,13 @@
|
|||
const folders = useAllFolders();
|
||||
|
||||
async function clearAllData() {
|
||||
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
|
||||
return;
|
||||
if (!confirm($_('uload.settings_route.confirm_clear'))) return;
|
||||
|
||||
await linkTable.clear();
|
||||
await uloadTagTable.clear();
|
||||
await uloadFolderTable.clear();
|
||||
await linkTagTable.clear();
|
||||
toast.success('Alle uLoad-Daten gelöscht');
|
||||
toast.success($_('uload.settings_route.toast_cleared'));
|
||||
}
|
||||
|
||||
async function exportData() {
|
||||
|
|
@ -47,59 +47,58 @@
|
|||
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
toast.success('Export heruntergeladen');
|
||||
toast.success($_('uload.settings_route.toast_exported'));
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
<title>uLoad-Einstellungen — Mana</title>
|
||||
<title>{$_('uload.settings_route.title')}</title>
|
||||
</svelte:head>
|
||||
|
||||
<RoutePage appId="uload" backHref="/uload">
|
||||
<div class="pane">
|
||||
<header class="bar">
|
||||
<div class="title">
|
||||
<strong>uLoad-Einstellungen</strong>
|
||||
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
|
||||
<strong>{$_('uload.settings_route.heading')}</strong>
|
||||
<span class="sub">{$_('uload.settings_route.subtitle')}</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten</h2>
|
||||
<h2>{$_('uload.settings_route.section_data')}</h2>
|
||||
<div class="stats">
|
||||
<div class="stat">
|
||||
<p class="stat-value">{links.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Links</p>
|
||||
<p class="stat-label">{$_('uload.settings_route.stat_links')}</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{tags.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Tags</p>
|
||||
<p class="stat-label">{$_('uload.settings_route.stat_tags')}</p>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<p class="stat-value">{folders.value?.length ?? 0}</p>
|
||||
<p class="stat-label">Ordner</p>
|
||||
<p class="stat-label">{$_('uload.settings_route.stat_folders')}</p>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Daten exportieren</h2>
|
||||
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
|
||||
<h2>{$_('uload.settings_route.section_export')}</h2>
|
||||
<p class="hint">{$_('uload.settings_route.export_hint')}</p>
|
||||
<button type="button" class="btn" onclick={exportData}>
|
||||
<DownloadSimple size={16} />
|
||||
JSON exportieren
|
||||
{$_('uload.settings_route.action_export')}
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="panel danger">
|
||||
<h2>Gefahrenzone</h2>
|
||||
<h2>{$_('uload.settings_route.section_danger')}</h2>
|
||||
<p class="hint">
|
||||
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
|
||||
bleiben erhalten.
|
||||
{$_('uload.settings_route.danger_hint')}
|
||||
</p>
|
||||
<button type="button" class="btn danger" onclick={clearAllData}>
|
||||
<Trash size={16} />
|
||||
Alle Daten löschen
|
||||
{$_('uload.settings_route.action_clear')}
|
||||
</button>
|
||||
</section>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -20,16 +20,17 @@
|
|||
|
||||
async function createTag() {
|
||||
if (!newName.trim()) return;
|
||||
const created = newName.trim();
|
||||
await uloadTagTable.add({
|
||||
id: crypto.randomUUID(),
|
||||
name: newName.trim(),
|
||||
name: created,
|
||||
slug: slugify(newName),
|
||||
color: newColor,
|
||||
icon: null,
|
||||
isPublic: false,
|
||||
usageCount: 0,
|
||||
} as LocalTag);
|
||||
toast.success(`Tag "${newName}" erstellt`);
|
||||
toast.success($_('uload.tags_route.toast_created', { values: { name: created } }));
|
||||
newName = '';
|
||||
newColor = '#6366f1';
|
||||
showCreateForm = false;
|
||||
|
|
@ -37,7 +38,7 @@
|
|||
|
||||
async function deleteTag(tag: { id: string; name: string }) {
|
||||
await uloadTagTable.delete(tag.id);
|
||||
toast.success(`Tag "${tag.name}" geloescht`);
|
||||
toast.success($_('uload.tags_route.toast_deleted', { values: { name: tag.name } }));
|
||||
}
|
||||
|
||||
async function updateTag() {
|
||||
|
|
@ -47,7 +48,7 @@
|
|||
slug: slugify(editingTag.name),
|
||||
color: editingTag.color,
|
||||
});
|
||||
toast.success('Tag aktualisiert');
|
||||
toast.success($_('uload.tags_route.toast_updated'));
|
||||
editingTag = null;
|
||||
}
|
||||
</script>
|
||||
|
|
@ -59,13 +60,13 @@
|
|||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5">
|
||||
<ArrowLeft size={20} class="text-muted-foreground" />
|
||||
</a>
|
||||
<h1 class="text-2xl font-bold text-white">Tags</h1>
|
||||
<h1 class="text-2xl font-bold text-white">{$_('uload.tags_route.heading')}</h1>
|
||||
</div>
|
||||
<button
|
||||
onclick={() => (showCreateForm = !showCreateForm)}
|
||||
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
||||
>
|
||||
{showCreateForm ? 'Ausblenden' : '+ Neuer Tag'}
|
||||
{showCreateForm ? $_('uload.tags_route.hide_form') : $_('uload.tags_route.show_form')}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -74,20 +75,20 @@
|
|||
<div class="flex items-end gap-4">
|
||||
<div class="flex-1">
|
||||
<label for="tag-name" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||
>Name</label
|
||||
>{$_('uload.tags_route.label_name')}</label
|
||||
>
|
||||
<input
|
||||
id="tag-name"
|
||||
type="text"
|
||||
bind:value={newName}
|
||||
placeholder="z.B. Social Media"
|
||||
placeholder={$_('uload.tags_route.placeholder_name')}
|
||||
class="w-full rounded-lg border border-border/10 bg-muted/5 px-4 py-2 text-white placeholder-white/30 focus:border-indigo-500 focus:outline-none"
|
||||
onkeydown={(e) => e.key === 'Enter' && createTag()}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="tag-color" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||
>Farbe</label
|
||||
>{$_('uload.tags_route.label_color')}</label
|
||||
>
|
||||
<input
|
||||
id="tag-color"
|
||||
|
|
@ -101,7 +102,7 @@
|
|||
disabled={!newName.trim()}
|
||||
class="rounded-lg bg-indigo-600 px-6 py-2 font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
|
||||
>
|
||||
Erstellen
|
||||
{$_('uload.tags_route.action_create')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -109,9 +110,11 @@
|
|||
|
||||
{#if !tags.value || tags.value.length === 0}
|
||||
<div class="rounded-xl border-2 border-dashed border-border/10 p-12 text-center">
|
||||
<p class="text-lg font-medium text-muted-foreground">Noch keine Tags</p>
|
||||
<p class="text-lg font-medium text-muted-foreground">
|
||||
{$_('uload.tags_route.empty_title')}
|
||||
</p>
|
||||
<p class="mt-1 text-sm text-muted-foreground">
|
||||
Erstelle Tags um deine Links zu organisieren.
|
||||
{$_('uload.tags_route.empty_desc')}
|
||||
</p>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -151,7 +154,11 @@
|
|||
<span class="font-medium text-white">{tag.name}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="text-sm text-muted-foreground">{getUsageCount(tag.id)} Links</span>
|
||||
<span class="text-sm text-muted-foreground"
|
||||
>{$_('uload.tags_route.links_count', {
|
||||
values: { count: getUsageCount(tag.id) },
|
||||
})}</span
|
||||
>
|
||||
<button
|
||||
onclick={() => (editingTag = { id: tag.id, name: tag.name, color: tag.color })}
|
||||
class="rounded p-1 text-muted-foreground opacity-0 transition-colors hover:bg-muted/10 hover:text-white group-hover:opacity-100"
|
||||
|
|
|
|||
|
|
@ -213,8 +213,6 @@
|
|||
"apps/mana/apps/web/src/lib/modules/todo/components/SyncIndicator.svelte": 4,
|
||||
"apps/mana/apps/web/src/lib/modules/todo/ListView.svelte": 1,
|
||||
"apps/mana/apps/web/src/lib/modules/todo/views/DetailView.svelte": 8,
|
||||
"apps/mana/apps/web/src/lib/modules/uload/ListView.svelte": 2,
|
||||
"apps/mana/apps/web/src/lib/modules/uload/views/DetailView.svelte": 7,
|
||||
"apps/mana/apps/web/src/lib/modules/website/components/BlockInspector.svelte": 3,
|
||||
"apps/mana/apps/web/src/lib/modules/website/components/DomainsSection.svelte": 4,
|
||||
"apps/mana/apps/web/src/lib/modules/website/components/GalleryInspector.svelte": 9,
|
||||
|
|
@ -356,11 +354,6 @@
|
|||
"apps/mana/apps/web/src/routes/(app)/times/projects/[id]/+page.svelte": 3,
|
||||
"apps/mana/apps/web/src/routes/(app)/todo/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/(app)/todo/settings/+page.svelte": 13,
|
||||
"apps/mana/apps/web/src/routes/(app)/uload/+page.svelte": 21,
|
||||
"apps/mana/apps/web/src/routes/(app)/uload/analytics/[id]/+page.svelte": 20,
|
||||
"apps/mana/apps/web/src/routes/(app)/uload/links/+page.svelte": 7,
|
||||
"apps/mana/apps/web/src/routes/(app)/uload/settings/+page.svelte": 6,
|
||||
"apps/mana/apps/web/src/routes/(app)/uload/tags/+page.svelte": 4,
|
||||
"apps/mana/apps/web/src/routes/(app)/wetter/+page.svelte": 2,
|
||||
"apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte": 1,
|
||||
"apps/mana/apps/web/src/routes/+error.svelte": 1,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue