mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-29 00:37:42 +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 (!newUrl) return;
|
||||||
|
|
||||||
if (!isValidUrl(newUrl)) {
|
if (!isValidUrl(newUrl)) {
|
||||||
toast.error('Bitte eine gueltige URL eingeben (mit https://)');
|
toast.error($_('uload.page.err_invalid_url_input'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const shortCode = newCustomCode || generateShortCode();
|
const shortCode = newCustomCode || generateShortCode();
|
||||||
|
|
||||||
if (newCustomCode && !isValidCustomCode(newCustomCode)) {
|
if (newCustomCode && !isValidCustomCode(newCustomCode)) {
|
||||||
toast.error('Custom Code darf nur Buchstaben, Zahlen, - und _ enthalten');
|
toast.error($_('uload.page.err_invalid_custom_code'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!(await isShortCodeUnique(shortCode))) {
|
if (!(await isShortCodeUnique(shortCode))) {
|
||||||
toast.error(`Short Code "${shortCode}" ist bereits vergeben`);
|
toast.error($_('uload.page.err_short_code_taken', { values: { code: shortCode } }));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxClicks = newMaxClicks ? parseInt(newMaxClicks) : null;
|
const maxClicks = newMaxClicks ? parseInt(newMaxClicks) : null;
|
||||||
if (maxClicks !== null && maxClicks < 1) {
|
if (maxClicks !== null && maxClicks < 1) {
|
||||||
toast.error('Max Klicks muss mindestens 1 sein');
|
toast.error($_('uload.page.err_max_clicks'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newExpiresAt && new Date(newExpiresAt) <= new Date()) {
|
if (newExpiresAt && new Date(newExpiresAt) <= new Date()) {
|
||||||
toast.error('Ablaufdatum muss in der Zukunft liegen');
|
toast.error($_('uload.page.err_expires_past'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -165,7 +165,7 @@
|
||||||
};
|
};
|
||||||
await encryptRecord('links', newRow);
|
await encryptRecord('links', newRow);
|
||||||
await linkTable.add(newRow);
|
await linkTable.add(newRow);
|
||||||
toast.success(`Link erstellt: ${shortCode}`);
|
toast.success($_('uload.page.toast_created', { values: { code: shortCode } }));
|
||||||
newUrl = '';
|
newUrl = '';
|
||||||
newTitle = '';
|
newTitle = '';
|
||||||
newCustomCode = '';
|
newCustomCode = '';
|
||||||
|
|
@ -195,18 +195,18 @@
|
||||||
if (!editingLink || !editUrl) return;
|
if (!editingLink || !editUrl) return;
|
||||||
|
|
||||||
if (!isValidUrl(editUrl)) {
|
if (!isValidUrl(editUrl)) {
|
||||||
toast.error('Bitte eine gueltige URL eingeben (mit https://)');
|
toast.error($_('uload.page.err_invalid_url_input'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const maxClicks = editMaxClicks ? parseInt(editMaxClicks) : null;
|
const maxClicks = editMaxClicks ? parseInt(editMaxClicks) : null;
|
||||||
if (maxClicks !== null && maxClicks < 1) {
|
if (maxClicks !== null && maxClicks < 1) {
|
||||||
toast.error('Max Klicks muss mindestens 1 sein');
|
toast.error($_('uload.page.err_max_clicks'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (editExpiresAt && new Date(editExpiresAt) <= new Date()) {
|
if (editExpiresAt && new Date(editExpiresAt) <= new Date()) {
|
||||||
toast.error('Ablaufdatum muss in der Zukunft liegen');
|
toast.error($_('uload.page.err_expires_past'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,7 +222,7 @@
|
||||||
};
|
};
|
||||||
await encryptRecord('links', diff);
|
await encryptRecord('links', diff);
|
||||||
await linkTable.update(editingLink.id, diff);
|
await linkTable.update(editingLink.id, diff);
|
||||||
toast.success('Link aktualisiert');
|
toast.success($_('uload.page.toast_updated'));
|
||||||
editingLink = null;
|
editingLink = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -231,14 +231,15 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLink(link: Link) {
|
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);
|
await linkTable.delete(link.id);
|
||||||
toast.success('Link geloescht');
|
toast.success($_('uload.page.toast_deleted'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function copyShortUrl(code: string) {
|
function copyShortUrl(code: string) {
|
||||||
navigator.clipboard.writeText(getShortUrl(code));
|
navigator.clipboard.writeText(getShortUrl(code));
|
||||||
toast.success('Link kopiert!');
|
toast.success($_('uload.page.toast_copied'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function downloadQr(code: string) {
|
function downloadQr(code: string) {
|
||||||
|
|
@ -256,7 +257,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>uLoad - Mana</title>
|
<title>{$_('uload.page.title')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<RoutePage appId="uload">
|
<RoutePage appId="uload">
|
||||||
|
|
@ -267,10 +268,13 @@
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">uLoad</h1>
|
<h1 class="text-2xl font-bold">uLoad</h1>
|
||||||
<p class="mt-1 text-sm opacity-60">
|
<p class="mt-1 text-sm opacity-60">
|
||||||
{filteredLinks.length} Links
|
{folders.length > 0
|
||||||
{#if folders.length > 0}
|
? $_('uload.page.counts', {
|
||||||
· {folders.length} Ordner
|
values: { links: filteredLinks.length, folders: folders.length },
|
||||||
{/if}
|
})
|
||||||
|
: $_('uload.page.counts_no_folders', {
|
||||||
|
values: { links: filteredLinks.length },
|
||||||
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
|
|
@ -278,13 +282,13 @@
|
||||||
href="/uload/links"
|
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"
|
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>
|
</a>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showCreateForm = !showCreateForm)}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -296,7 +300,9 @@
|
||||||
>
|
>
|
||||||
<div class="grid gap-4 md:grid-cols-2">
|
<div class="grid gap-4 md:grid-cols-2">
|
||||||
<div class="md:col-span-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
|
<input
|
||||||
id="url"
|
id="url"
|
||||||
type="url"
|
type="url"
|
||||||
|
|
@ -307,23 +313,26 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newTitle}
|
bind:value={newTitle}
|
||||||
placeholder="Mein Link"
|
placeholder={$_('uload.page.placeholder_title')}
|
||||||
class={inputClass}
|
class={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
id="code"
|
id="code"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newCustomCode}
|
bind:value={newCustomCode}
|
||||||
placeholder="mein-link"
|
placeholder={$_('uload.page.placeholder_custom_code')}
|
||||||
class={inputClass}
|
class={inputClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -337,13 +346,13 @@
|
||||||
<span class="transition-transform {showAdvanced ? 'rotate-90' : ''}"
|
<span class="transition-transform {showAdvanced ? 'rotate-90' : ''}"
|
||||||
><CaretRight size={16} /></span
|
><CaretRight size={16} /></span
|
||||||
>
|
>
|
||||||
Erweitert
|
{$_('uload.page.section_advanced')}
|
||||||
</button>
|
</button>
|
||||||
{#if showAdvanced}
|
{#if showAdvanced}
|
||||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="expires" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="expires" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Ablaufdatum</label
|
>{$_('uload.page.label_expires')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="expires"
|
id="expires"
|
||||||
|
|
@ -354,25 +363,25 @@
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="password" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="password" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Passwort</label
|
>{$_('uload.page.label_password')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="password"
|
id="password"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newPassword}
|
bind:value={newPassword}
|
||||||
placeholder="Optional"
|
placeholder={$_('uload.page.placeholder_optional')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="maxclicks" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="maxclicks" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Max Klicks</label
|
>{$_('uload.page.label_max_clicks')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="maxclicks"
|
id="maxclicks"
|
||||||
type="number"
|
type="number"
|
||||||
bind:value={newMaxClicks}
|
bind:value={newMaxClicks}
|
||||||
placeholder="Unbegrenzt"
|
placeholder={$_('uload.page.placeholder_unlimited')}
|
||||||
min="1"
|
min="1"
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
|
|
@ -388,43 +397,43 @@
|
||||||
<span class="transition-transform {showUtm ? 'rotate-90' : ''}"
|
<span class="transition-transform {showUtm ? 'rotate-90' : ''}"
|
||||||
><CaretRight size={16} /></span
|
><CaretRight size={16} /></span
|
||||||
>
|
>
|
||||||
UTM-Parameter
|
{$_('uload.page.section_utm')}
|
||||||
</button>
|
</button>
|
||||||
{#if showUtm}
|
{#if showUtm}
|
||||||
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
<div class="mt-3 grid gap-3 md:grid-cols-3">
|
||||||
<div>
|
<div>
|
||||||
<label for="utm-source" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="utm-source" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Source</label
|
>{$_('uload.page.label_source')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="utm-source"
|
id="utm-source"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newUtmSource}
|
bind:value={newUtmSource}
|
||||||
placeholder="newsletter"
|
placeholder={$_('uload.page.placeholder_source')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="utm-medium" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="utm-medium" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Medium</label
|
>{$_('uload.page.label_medium')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="utm-medium"
|
id="utm-medium"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newUtmMedium}
|
bind:value={newUtmMedium}
|
||||||
placeholder="email"
|
placeholder={$_('uload.page.placeholder_medium')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="utm-campaign" class="mb-1 block text-xs font-medium opacity-70"
|
<label for="utm-campaign" class="mb-1 block text-xs font-medium opacity-70"
|
||||||
>Campaign</label
|
>{$_('uload.page.label_campaign')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="utm-campaign"
|
id="utm-campaign"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newUtmCampaign}
|
bind:value={newUtmCampaign}
|
||||||
placeholder="spring-2026"
|
placeholder={$_('uload.page.placeholder_campaign')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -437,7 +446,7 @@
|
||||||
disabled={!newUrl}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -453,18 +462,18 @@
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={searchQuery}
|
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"
|
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>
|
</div>
|
||||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||||
<option value="all">Alle</option>
|
<option value="all">{$_('uload.page.option_all')}</option>
|
||||||
<option value="active">Aktiv</option>
|
<option value="active">{$_('uload.page.option_active')}</option>
|
||||||
<option value="inactive">Inaktiv</option>
|
<option value="inactive">{$_('uload.page.option_inactive')}</option>
|
||||||
</select>
|
</select>
|
||||||
{#if folders.length > 0}
|
{#if folders.length > 0}
|
||||||
<select bind:value={selectedFolderId} class={inputSmClass} style="max-width: 160px">
|
<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}
|
{#each folders as folder}
|
||||||
<option value={folder.id}>{folder.name}</option>
|
<option value={folder.id}>{folder.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -484,13 +493,13 @@
|
||||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
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" />
|
<LinkIcon size={48} class="mx-auto mb-4 opacity-20" />
|
||||||
<p class="text-lg font-medium opacity-60">Noch keine Links</p>
|
<p class="text-lg font-medium opacity-60">{$_('uload.page.empty_title')}</p>
|
||||||
<p class="mt-1 text-sm opacity-40">Erstelle deinen ersten gekuerzten Link!</p>
|
<p class="mt-1 text-sm opacity-40">{$_('uload.page.empty_desc')}</p>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showCreateForm = true)}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -516,19 +525,21 @@
|
||||||
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
||||||
<span
|
<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"
|
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}
|
||||||
{#if link.password}
|
{#if link.password}
|
||||||
<span
|
<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"
|
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}
|
||||||
{#if link.expiresAt}
|
{#if link.expiresAt}
|
||||||
<span
|
<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"
|
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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -552,7 +563,7 @@
|
||||||
<a
|
<a
|
||||||
href="/uload/analytics/{link.id}"
|
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"
|
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} />
|
<ChartBar size={16} />
|
||||||
{link.clickCount}
|
{link.clickCount}
|
||||||
|
|
@ -560,28 +571,30 @@
|
||||||
<button
|
<button
|
||||||
onclick={() => copyShortUrl(link.shortCode)}
|
onclick={() => copyShortUrl(link.shortCode)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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} />
|
<Copy size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => (qrLink = link)}
|
onclick={() => (qrLink = link)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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} />
|
<QrCode size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => openEdit(link)}
|
onclick={() => openEdit(link)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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} />
|
<PencilSimple size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => toggleActive(link)}
|
onclick={() => toggleActive(link)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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
|
<Lightning
|
||||||
size={16}
|
size={16}
|
||||||
|
|
@ -591,7 +604,7 @@
|
||||||
<button
|
<button
|
||||||
onclick={() => deleteLink(link)}
|
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"
|
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} />
|
<Trash size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -620,7 +633,7 @@
|
||||||
role="none"
|
role="none"
|
||||||
>
|
>
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<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
|
<button
|
||||||
onclick={() => (editingLink = null)}
|
onclick={() => (editingLink = null)}
|
||||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||||
|
|
@ -631,50 +644,57 @@
|
||||||
|
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<div>
|
<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} />
|
<input id="edit-url" type="url" bind:value={editUrl} class={inputClass} />
|
||||||
</div>
|
</div>
|
||||||
<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} />
|
<input id="edit-title" type="text" bind:value={editTitle} class={inputClass} />
|
||||||
</div>
|
</div>
|
||||||
<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">
|
<div class="flex items-center gap-2">
|
||||||
<span class="text-sm opacity-50">/{editingLink.shortCode}</span>
|
<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>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
<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">
|
<div class="grid gap-3 md:grid-cols-3">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editUtmSource}
|
bind:value={editUtmSource}
|
||||||
placeholder="Source"
|
placeholder={$_('uload.page.label_source')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editUtmMedium}
|
bind:value={editUtmMedium}
|
||||||
placeholder="Medium"
|
placeholder={$_('uload.page.label_medium')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editUtmCampaign}
|
bind:value={editUtmCampaign}
|
||||||
placeholder="Campaign"
|
placeholder={$_('uload.page.label_campaign')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="border-t border-border-strong pt-4 dark:border-border">
|
<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 class="grid gap-3 md:grid-cols-3">
|
||||||
<div>
|
<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
|
<input
|
||||||
id="uload-expires-at"
|
id="uload-expires-at"
|
||||||
|
|
@ -684,22 +704,26 @@
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
id="uload-password"
|
id="uload-password"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={editPassword}
|
bind:value={editPassword}
|
||||||
placeholder="Optional"
|
placeholder={$_('uload.page.placeholder_optional')}
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<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
|
<input
|
||||||
id="uload-max-clicks"
|
id="uload-max-clicks"
|
||||||
type="number"
|
type="number"
|
||||||
bind:value={editMaxClicks}
|
bind:value={editMaxClicks}
|
||||||
placeholder="Unbegrenzt"
|
placeholder={$_('uload.page.placeholder_unlimited')}
|
||||||
min="1"
|
min="1"
|
||||||
class={inputSmClass}
|
class={inputSmClass}
|
||||||
/>
|
/>
|
||||||
|
|
@ -713,14 +737,14 @@
|
||||||
onclick={() => (editingLink = null)}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
onclick={saveEdit}
|
onclick={saveEdit}
|
||||||
disabled={!editUrl}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -742,7 +766,7 @@
|
||||||
role="none"
|
role="none"
|
||||||
>
|
>
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<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
|
<button
|
||||||
onclick={() => (qrLink = null)}
|
onclick={() => (qrLink = null)}
|
||||||
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
class="rounded-lg p-1 hover:bg-muted dark:hover:bg-muted"
|
||||||
|
|
@ -755,7 +779,7 @@
|
||||||
<div class="rounded-lg bg-white p-4">
|
<div class="rounded-lg bg-white p-4">
|
||||||
<img
|
<img
|
||||||
src="{QR_API}/?size=200x200&data={encodeURIComponent(getShortUrl(qrLink.shortCode))}"
|
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"
|
class="h-48 w-48"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -765,13 +789,13 @@
|
||||||
onclick={() => copyShortUrl(qrLink!.shortCode)}
|
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"
|
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>
|
||||||
<button
|
<button
|
||||||
onclick={() => downloadQr(qrLink!.shortCode)}
|
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"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
import { formatDate } from '$lib/i18n/format';
|
import { formatDate } from '$lib/i18n/format';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
@ -71,18 +72,22 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Analytics - uLoad - Mana</title>
|
<title>{$_('uload.analytics_route.title')}</title>
|
||||||
</svelte:head>
|
</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">
|
<div class="mx-auto max-w-4xl p-4">
|
||||||
<!-- Header -->
|
<!-- Header -->
|
||||||
<div class="mb-6 flex items-center gap-4">
|
<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" />
|
<CaretLeft size={20} class="text-muted-foreground" />
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<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}
|
{#if link}
|
||||||
<p class="mt-1 text-sm text-muted-foreground">
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
<span class="font-mono text-indigo-400">/{link.shortCode}</span>
|
<span class="font-mono text-indigo-400">/{link.shortCode}</span>
|
||||||
|
|
@ -100,35 +105,45 @@
|
||||||
</div>
|
</div>
|
||||||
{:else if !link}
|
{:else if !link}
|
||||||
<div class="rounded-xl border border-border/10 p-12 text-center">
|
<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>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<!-- Stats Overview -->
|
<!-- Stats Overview -->
|
||||||
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
<div class="mb-6 grid gap-4 sm:grid-cols-4">
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
<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">
|
<p class="mt-1 text-3xl font-bold text-white">
|
||||||
{stats?.totalClicks ?? link.clickCount}
|
{stats?.totalClicks ?? link.clickCount}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
<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">
|
<p class="mt-1 text-3xl font-bold text-white">
|
||||||
{stats?.uniqueVisitors ?? '-'}
|
{stats?.uniqueVisitors ?? '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
<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">
|
<p class="mt-1 text-3xl font-bold">
|
||||||
{#if link.isActive}
|
{#if link.isActive}
|
||||||
<span class="text-green-400">Aktiv</span>
|
<span class="text-green-400">{$_('uload.analytics_route.status_active')}</span>
|
||||||
{:else}
|
{:else}
|
||||||
<span class="text-muted-foreground/70">Inaktiv</span>
|
<span class="text-muted-foreground/70"
|
||||||
|
>{$_('uload.analytics_route.status_inactive')}</span
|
||||||
|
>
|
||||||
{/if}
|
{/if}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-5">
|
<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">
|
<p class="mt-1 text-lg font-bold text-white">
|
||||||
{formatDate(new Date(link.createdAt))}
|
{formatDate(new Date(link.createdAt))}
|
||||||
</p>
|
</p>
|
||||||
|
|
@ -137,10 +152,13 @@
|
||||||
|
|
||||||
<!-- Link Details -->
|
<!-- Link Details -->
|
||||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
<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="space-y-3">
|
||||||
<div class="flex items-center justify-between text-sm">
|
<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
|
<a
|
||||||
href={link.originalUrl}
|
href={link.originalUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
|
@ -152,31 +170,37 @@
|
||||||
</div>
|
</div>
|
||||||
{#if link.title}
|
{#if link.title}
|
||||||
<div class="flex items-center justify-between text-sm">
|
<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>
|
<span class="text-white">{link.title}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
{#if link.utmSource || link.utmMedium || link.utmCampaign}
|
||||||
<div class="border-t border-border/10 pt-3">
|
<div class="border-t border-border/10 pt-3">
|
||||||
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
<p class="mb-2 text-xs font-medium uppercase tracking-wider text-muted-foreground">
|
||||||
UTM-Parameter
|
{$_('uload.analytics_route.label_utm_params')}
|
||||||
</p>
|
</p>
|
||||||
<div class="grid gap-2 sm:grid-cols-3">
|
<div class="grid gap-2 sm:grid-cols-3">
|
||||||
{#if link.utmSource}
|
{#if link.utmSource}
|
||||||
<div class="text-sm text-foreground">
|
<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}
|
{link.utmSource}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.utmMedium}
|
{#if link.utmMedium}
|
||||||
<div class="text-sm text-foreground">
|
<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}
|
{link.utmMedium}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.utmCampaign}
|
{#if link.utmCampaign}
|
||||||
<div class="text-sm text-foreground">
|
<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}
|
{link.utmCampaign}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -185,20 +209,30 @@
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.expiresAt}
|
{#if link.expiresAt}
|
||||||
<div class="flex items-center justify-between text-sm">
|
<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>
|
<span class="text-white">{formatDate(new Date(link.expiresAt))}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.maxClicks}
|
{#if link.maxClicks}
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">Max Klicks</span>
|
<span class="text-muted-foreground"
|
||||||
<span class="text-white">{link.clickCount} / {link.maxClicks}</span>
|
>{$_('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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
{#if link.password}
|
{#if link.password}
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="text-muted-foreground">Passwortgeschuetzt</span>
|
<span class="text-muted-foreground"
|
||||||
<span class="text-white">Ja</span>
|
>{$_('uload.analytics_route.label_password_protected')}</span
|
||||||
|
>
|
||||||
|
<span class="text-white">{$_('uload.analytics_route.yes')}</span>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -207,7 +241,9 @@
|
||||||
<!-- Timeline -->
|
<!-- Timeline -->
|
||||||
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
<div class="mb-6 rounded-xl border border-border/10 bg-muted/5 p-6">
|
||||||
<div class="mb-4 flex items-center justify-between">
|
<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">
|
<div class="flex gap-1">
|
||||||
{#each [7, 30, 90] as d}
|
{#each [7, 30, 90] as d}
|
||||||
<button
|
<button
|
||||||
|
|
@ -216,7 +252,7 @@
|
||||||
? 'bg-indigo-600 text-white'
|
? 'bg-indigo-600 text-white'
|
||||||
: 'bg-muted/10 text-muted-foreground hover:bg-muted/15'}"
|
: 'bg-muted/10 text-muted-foreground hover:bg-muted/15'}"
|
||||||
>
|
>
|
||||||
{d}T
|
{$_('uload.analytics_route.days_unit', { values: { days: d } })}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -244,15 +280,17 @@
|
||||||
{:else if !serverAvailable}
|
{:else if !serverAvailable}
|
||||||
<div class="py-8 text-center">
|
<div class="py-8 text-center">
|
||||||
<p class="text-sm text-muted-foreground">
|
<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>
|
||||||
<p class="mt-1 text-xs text-muted-foreground/70">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
<p class="py-8 text-center text-sm text-muted-foreground">
|
<p class="py-8 text-center text-sm text-muted-foreground">
|
||||||
Noch keine Daten fuer diesen Zeitraum
|
{$_('uload.analytics_route.empty_period')}
|
||||||
</p>
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -262,13 +300,17 @@
|
||||||
<div class="grid gap-6 md:grid-cols-3">
|
<div class="grid gap-6 md:grid-cols-3">
|
||||||
<!-- Devices -->
|
<!-- Devices -->
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
<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}
|
{#if devices.length > 0}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each devices as d}
|
{#each devices as d}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 flex items-center justify-between text-sm">
|
<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">
|
<span class="font-medium text-white">
|
||||||
{Math.round((d.count / totalDevices) * 100)}%
|
{Math.round((d.count / totalDevices) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -283,38 +325,48 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Referrers -->
|
<!-- Referrers -->
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
<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}
|
{#if referrers.length > 0}
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
{#each referrers.slice(0, 8) as r}
|
{#each referrers.slice(0, 8) as r}
|
||||||
<div class="flex items-center justify-between text-sm">
|
<div class="flex items-center justify-between text-sm">
|
||||||
<span class="max-w-[140px] truncate text-foreground">
|
<span class="max-w-[140px] truncate text-foreground">
|
||||||
{r.referer || 'Direkt'}
|
{r.referer || $_('uload.analytics_route.direct')}
|
||||||
</span>
|
</span>
|
||||||
<span class="font-medium tabular-nums text-white">{r.count}</span>
|
<span class="font-medium tabular-nums text-white">{r.count}</span>
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Countries -->
|
<!-- Countries -->
|
||||||
<div class="rounded-xl border border-border/10 bg-muted/5 p-6">
|
<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}
|
{#if countries.length > 0}
|
||||||
<div class="space-y-3">
|
<div class="space-y-3">
|
||||||
{#each countries.slice(0, 8) as c}
|
{#each countries.slice(0, 8) as c}
|
||||||
<div>
|
<div>
|
||||||
<div class="mb-1 flex items-center justify-between text-sm">
|
<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">
|
<span class="font-medium text-white">
|
||||||
{Math.round((c.count / totalCountries) * 100)}%
|
{Math.round((c.count / totalCountries) * 100)}%
|
||||||
</span>
|
</span>
|
||||||
|
|
@ -329,7 +381,9 @@
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
import {
|
import {
|
||||||
useAllLinks,
|
useAllLinks,
|
||||||
useAllTags,
|
useAllTags,
|
||||||
|
|
@ -68,7 +69,7 @@
|
||||||
|
|
||||||
function copyShortUrl(code: string) {
|
function copyShortUrl(code: string) {
|
||||||
navigator.clipboard.writeText(getShortUrl(code));
|
navigator.clipboard.writeText(getShortUrl(code));
|
||||||
toast.success('Link kopiert!');
|
toast.success($_('uload.links_route.toast_copied'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleActive(link: Link) {
|
async function toggleActive(link: Link) {
|
||||||
|
|
@ -76,9 +77,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteLink(link: Link) {
|
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);
|
await linkTable.delete(link.id);
|
||||||
toast.success('Link geloescht');
|
toast.success($_('uload.links_route.toast_deleted_single'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bulk actions
|
// Bulk actions
|
||||||
|
|
@ -101,22 +103,24 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkDelete() {
|
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) {
|
for (const id of selectedIds) {
|
||||||
await linkTable.delete(id);
|
await linkTable.delete(id);
|
||||||
}
|
}
|
||||||
toast.success(`${selectedIds.size} Links geloescht`);
|
toast.success($_('uload.links_route.toast_bulk_deleted', { values: { count } }));
|
||||||
selectedIds.clear();
|
selectedIds.clear();
|
||||||
selectedIds = selectedIds;
|
selectedIds = selectedIds;
|
||||||
selectMode = false;
|
selectMode = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function bulkToggleActive() {
|
async function bulkToggleActive() {
|
||||||
|
const count = selectedIds.size;
|
||||||
for (const id of selectedIds) {
|
for (const id of selectedIds) {
|
||||||
const link = filteredLinks.find((l) => l.id === id);
|
const link = filteredLinks.find((l) => l.id === id);
|
||||||
if (link) await linkTable.update(id, { isActive: !link.isActive });
|
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.clear();
|
||||||
selectedIds = selectedIds;
|
selectedIds = selectedIds;
|
||||||
selectMode = false;
|
selectMode = false;
|
||||||
|
|
@ -127,7 +131,7 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>Alle Links - uLoad - Mana</title>
|
<title>{$_('uload.links_route.title')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<RoutePage appId="uload" backHref="/uload">
|
<RoutePage appId="uload" backHref="/uload">
|
||||||
|
|
@ -139,13 +143,13 @@
|
||||||
<a
|
<a
|
||||||
href="/uload"
|
href="/uload"
|
||||||
class="rounded-lg p-2 hover:bg-muted dark:hover:bg-muted"
|
class="rounded-lg p-2 hover:bg-muted dark:hover:bg-muted"
|
||||||
title="Zurueck"
|
title={$_('uload.links_route.action_back_title')}
|
||||||
>
|
>
|
||||||
<ArrowLeft size={20} />
|
<ArrowLeft size={20} />
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
<h1 class="text-2xl font-bold">
|
<h1 class="text-2xl font-bold">
|
||||||
Alle Links
|
{$_('uload.links_route.heading')}
|
||||||
{#if filteredLinks.length > 0}
|
{#if filteredLinks.length > 0}
|
||||||
<span class="ml-1 text-xl opacity-50">({filteredLinks.length})</span>
|
<span class="ml-1 text-xl opacity-50">({filteredLinks.length})</span>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -165,7 +169,9 @@
|
||||||
? 'bg-indigo-600 text-white'
|
? 'bg-indigo-600 text-white'
|
||||||
: 'hover:bg-muted dark:border-border dark:hover:bg-muted'}"
|
: '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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -180,18 +186,18 @@
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={searchQuery}
|
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"
|
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>
|
</div>
|
||||||
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
<select bind:value={selectedStatus} class={inputSmClass} style="max-width: 140px">
|
||||||
<option value="all">Alle</option>
|
<option value="all">{$_('uload.links_route.option_all')}</option>
|
||||||
<option value="active">Aktiv</option>
|
<option value="active">{$_('uload.links_route.option_active')}</option>
|
||||||
<option value="inactive">Inaktiv</option>
|
<option value="inactive">{$_('uload.links_route.option_inactive')}</option>
|
||||||
</select>
|
</select>
|
||||||
{#if folders.length > 0}
|
{#if folders.length > 0}
|
||||||
<select bind:value={selectedFolderId} class={inputSmClass} style="max-width: 160px">
|
<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}
|
{#each folders as folder}
|
||||||
<option value={folder.id}>{folder.name}</option>
|
<option value={folder.id}>{folder.name}</option>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
@ -211,18 +217,22 @@
|
||||||
onchange={toggleSelectAll}
|
onchange={toggleSelectAll}
|
||||||
class="h-4 w-4 rounded"
|
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>
|
</label>
|
||||||
<div class="h-4 w-px bg-indigo-300 dark:bg-indigo-700"></div>
|
<div class="h-4 w-px bg-indigo-300 dark:bg-indigo-700"></div>
|
||||||
<button
|
<button
|
||||||
onclick={bulkToggleActive}
|
onclick={bulkToggleActive}
|
||||||
class="rounded px-3 py-1 text-sm font-medium hover:bg-indigo-100 dark:hover:bg-indigo-800"
|
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
|
<button
|
||||||
onclick={bulkDelete}
|
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"
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
@ -239,11 +249,11 @@
|
||||||
class="rounded-xl border-2 border-dashed border-border-strong p-12 text-center dark:border-border"
|
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" />
|
<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}
|
{#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}
|
{: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}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -301,7 +311,7 @@
|
||||||
<a
|
<a
|
||||||
href="/uload/analytics/{link.id}"
|
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"
|
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} />
|
<ChartBar size={16} />
|
||||||
{link.clickCount}
|
{link.clickCount}
|
||||||
|
|
@ -309,14 +319,16 @@
|
||||||
<button
|
<button
|
||||||
onclick={() => copyShortUrl(link.shortCode)}
|
onclick={() => copyShortUrl(link.shortCode)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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} />
|
<Copy size={16} />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => toggleActive(link)}
|
onclick={() => toggleActive(link)}
|
||||||
class="rounded-lg p-2 opacity-0 transition-colors hover:bg-muted group-hover:opacity-100 dark:hover:bg-muted"
|
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
|
<Lightning
|
||||||
size={16}
|
size={16}
|
||||||
|
|
@ -326,7 +338,7 @@
|
||||||
<button
|
<button
|
||||||
onclick={() => deleteLink(link)}
|
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"
|
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} />
|
<Trash size={16} />
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
Reached via the ⚙ button in the uLoad module; not a workbench card.
|
Reached via the ⚙ button in the uLoad module; not a workbench card.
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { _ } from 'svelte-i18n';
|
||||||
import { Trash, DownloadSimple } from '@mana/shared-icons';
|
import { Trash, DownloadSimple } from '@mana/shared-icons';
|
||||||
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
import { linkTable, uloadTagTable, uloadFolderTable, linkTagTable } from '$lib/modules/uload';
|
||||||
import { decryptRecords } from '$lib/data/crypto';
|
import { decryptRecords } from '$lib/data/crypto';
|
||||||
|
|
@ -15,14 +16,13 @@
|
||||||
const folders = useAllFolders();
|
const folders = useAllFolders();
|
||||||
|
|
||||||
async function clearAllData() {
|
async function clearAllData() {
|
||||||
if (!confirm('Alle lokalen uLoad-Daten löschen? Dies kann nicht rückgängig gemacht werden.'))
|
if (!confirm($_('uload.settings_route.confirm_clear'))) return;
|
||||||
return;
|
|
||||||
|
|
||||||
await linkTable.clear();
|
await linkTable.clear();
|
||||||
await uloadTagTable.clear();
|
await uloadTagTable.clear();
|
||||||
await uloadFolderTable.clear();
|
await uloadFolderTable.clear();
|
||||||
await linkTagTable.clear();
|
await linkTagTable.clear();
|
||||||
toast.success('Alle uLoad-Daten gelöscht');
|
toast.success($_('uload.settings_route.toast_cleared'));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function exportData() {
|
async function exportData() {
|
||||||
|
|
@ -47,59 +47,58 @@
|
||||||
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
|
a.download = `uload-export-${new Date().toISOString().slice(0, 10)}.json`;
|
||||||
a.click();
|
a.click();
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
toast.success('Export heruntergeladen');
|
toast.success($_('uload.settings_route.toast_exported'));
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
<title>uLoad-Einstellungen — Mana</title>
|
<title>{$_('uload.settings_route.title')}</title>
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<RoutePage appId="uload" backHref="/uload">
|
<RoutePage appId="uload" backHref="/uload">
|
||||||
<div class="pane">
|
<div class="pane">
|
||||||
<header class="bar">
|
<header class="bar">
|
||||||
<div class="title">
|
<div class="title">
|
||||||
<strong>uLoad-Einstellungen</strong>
|
<strong>{$_('uload.settings_route.heading')}</strong>
|
||||||
<span class="sub">Datenübersicht · Export · Gefahrenzone</span>
|
<span class="sub">{$_('uload.settings_route.subtitle')}</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Daten</h2>
|
<h2>{$_('uload.settings_route.section_data')}</h2>
|
||||||
<div class="stats">
|
<div class="stats">
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<p class="stat-value">{links.value?.length ?? 0}</p>
|
<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>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<p class="stat-value">{tags.value?.length ?? 0}</p>
|
<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>
|
||||||
<div class="stat">
|
<div class="stat">
|
||||||
<p class="stat-value">{folders.value?.length ?? 0}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel">
|
<section class="panel">
|
||||||
<h2>Daten exportieren</h2>
|
<h2>{$_('uload.settings_route.section_export')}</h2>
|
||||||
<p class="hint">Alle Links, Tags und Ordner als JSON-Datei herunterladen.</p>
|
<p class="hint">{$_('uload.settings_route.export_hint')}</p>
|
||||||
<button type="button" class="btn" onclick={exportData}>
|
<button type="button" class="btn" onclick={exportData}>
|
||||||
<DownloadSimple size={16} />
|
<DownloadSimple size={16} />
|
||||||
JSON exportieren
|
{$_('uload.settings_route.action_export')}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="panel danger">
|
<section class="panel danger">
|
||||||
<h2>Gefahrenzone</h2>
|
<h2>{$_('uload.settings_route.section_danger')}</h2>
|
||||||
<p class="hint">
|
<p class="hint">
|
||||||
Löscht alle lokalen uLoad-Daten (Links, Tags, Ordner). Synchronisierte Daten auf dem Server
|
{$_('uload.settings_route.danger_hint')}
|
||||||
bleiben erhalten.
|
|
||||||
</p>
|
</p>
|
||||||
<button type="button" class="btn danger" onclick={clearAllData}>
|
<button type="button" class="btn danger" onclick={clearAllData}>
|
||||||
<Trash size={16} />
|
<Trash size={16} />
|
||||||
Alle Daten löschen
|
{$_('uload.settings_route.action_clear')}
|
||||||
</button>
|
</button>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -20,16 +20,17 @@
|
||||||
|
|
||||||
async function createTag() {
|
async function createTag() {
|
||||||
if (!newName.trim()) return;
|
if (!newName.trim()) return;
|
||||||
|
const created = newName.trim();
|
||||||
await uloadTagTable.add({
|
await uloadTagTable.add({
|
||||||
id: crypto.randomUUID(),
|
id: crypto.randomUUID(),
|
||||||
name: newName.trim(),
|
name: created,
|
||||||
slug: slugify(newName),
|
slug: slugify(newName),
|
||||||
color: newColor,
|
color: newColor,
|
||||||
icon: null,
|
icon: null,
|
||||||
isPublic: false,
|
isPublic: false,
|
||||||
usageCount: 0,
|
usageCount: 0,
|
||||||
} as LocalTag);
|
} as LocalTag);
|
||||||
toast.success(`Tag "${newName}" erstellt`);
|
toast.success($_('uload.tags_route.toast_created', { values: { name: created } }));
|
||||||
newName = '';
|
newName = '';
|
||||||
newColor = '#6366f1';
|
newColor = '#6366f1';
|
||||||
showCreateForm = false;
|
showCreateForm = false;
|
||||||
|
|
@ -37,7 +38,7 @@
|
||||||
|
|
||||||
async function deleteTag(tag: { id: string; name: string }) {
|
async function deleteTag(tag: { id: string; name: string }) {
|
||||||
await uloadTagTable.delete(tag.id);
|
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() {
|
async function updateTag() {
|
||||||
|
|
@ -47,7 +48,7 @@
|
||||||
slug: slugify(editingTag.name),
|
slug: slugify(editingTag.name),
|
||||||
color: editingTag.color,
|
color: editingTag.color,
|
||||||
});
|
});
|
||||||
toast.success('Tag aktualisiert');
|
toast.success($_('uload.tags_route.toast_updated'));
|
||||||
editingTag = null;
|
editingTag = null;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -59,13 +60,13 @@
|
||||||
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5">
|
<a href="/uload" class="rounded-lg p-2 transition-colors hover:bg-muted/5">
|
||||||
<ArrowLeft size={20} class="text-muted-foreground" />
|
<ArrowLeft size={20} class="text-muted-foreground" />
|
||||||
</a>
|
</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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showCreateForm = !showCreateForm)}
|
onclick={() => (showCreateForm = !showCreateForm)}
|
||||||
class="rounded-lg bg-indigo-600 px-4 py-2 text-sm font-medium text-white hover:bg-indigo-700"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -74,20 +75,20 @@
|
||||||
<div class="flex items-end gap-4">
|
<div class="flex items-end gap-4">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
<label for="tag-name" class="mb-1 block text-sm font-medium text-muted-foreground"
|
<label for="tag-name" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||||
>Name</label
|
>{$_('uload.tags_route.label_name')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="tag-name"
|
id="tag-name"
|
||||||
type="text"
|
type="text"
|
||||||
bind:value={newName}
|
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"
|
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()}
|
onkeydown={(e) => e.key === 'Enter' && createTag()}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label for="tag-color" class="mb-1 block text-sm font-medium text-muted-foreground"
|
<label for="tag-color" class="mb-1 block text-sm font-medium text-muted-foreground"
|
||||||
>Farbe</label
|
>{$_('uload.tags_route.label_color')}</label
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
id="tag-color"
|
id="tag-color"
|
||||||
|
|
@ -101,7 +102,7 @@
|
||||||
disabled={!newName.trim()}
|
disabled={!newName.trim()}
|
||||||
class="rounded-lg bg-indigo-600 px-6 py-2 font-medium text-white hover:bg-indigo-700 disabled:opacity-50"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -109,9 +110,11 @@
|
||||||
|
|
||||||
{#if !tags.value || tags.value.length === 0}
|
{#if !tags.value || tags.value.length === 0}
|
||||||
<div class="rounded-xl border-2 border-dashed border-border/10 p-12 text-center">
|
<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">
|
<p class="mt-1 text-sm text-muted-foreground">
|
||||||
Erstelle Tags um deine Links zu organisieren.
|
{$_('uload.tags_route.empty_desc')}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
|
|
@ -151,7 +154,11 @@
|
||||||
<span class="font-medium text-white">{tag.name}</span>
|
<span class="font-medium text-white">{tag.name}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<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
|
<button
|
||||||
onclick={() => (editingTag = { id: tag.id, name: tag.name, color: tag.color })}
|
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"
|
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/components/SyncIndicator.svelte": 4,
|
||||||
"apps/mana/apps/web/src/lib/modules/todo/ListView.svelte": 1,
|
"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/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/BlockInspector.svelte": 3,
|
||||||
"apps/mana/apps/web/src/lib/modules/website/components/DomainsSection.svelte": 4,
|
"apps/mana/apps/web/src/lib/modules/website/components/DomainsSection.svelte": 4,
|
||||||
"apps/mana/apps/web/src/lib/modules/website/components/GalleryInspector.svelte": 9,
|
"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)/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/+page.svelte": 1,
|
||||||
"apps/mana/apps/web/src/routes/(app)/todo/settings/+page.svelte": 13,
|
"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/(app)/wetter/+page.svelte": 2,
|
||||||
"apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte": 1,
|
"apps/mana/apps/web/src/routes/(auth)/reset-password/+page.svelte": 1,
|
||||||
"apps/mana/apps/web/src/routes/+error.svelte": 1,
|
"apps/mana/apps/web/src/routes/+error.svelte": 1,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue