mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 03:01:09 +02:00
Eight more package-level type errors that all came from the same
small handful of patterns.
Modal escape-key handlers calling click-style functions
Four modals (AuthGateModal, GuestWelcomeModal, ConfirmationPopover,
ShareModal) had `onkeydown={(e) => { if (e.key === 'Escape')
handleBackdropClick(); }}` — but handleBackdropClick took a MouseEvent
parameter, so the no-arg call failed with "Expected 1 arguments,
got 0". Fix: route the keyboard escape path through the right
no-arg helper (`onClose` / `handleClose` / `handleContinueAsGuest`)
or pass the keyboard event through with a cast for the popover
trigger that genuinely shares its handler with the click path.
WallpaperModal $derived
`currentLayout` and `currentBackground` were declared with
`$derived(() => {...})` — passing a function expression. The
variant that takes a thunk is `$derived.by(...)`; plain `$derived`
expects a single value expression. Result: the variables held the
arrow function itself, the call sites had to invoke them as
`currentLayout()`, and TS rejected the function value where Layout
was expected. Switch to `$derived.by`, drop the call-site parens.
TagList.svelte
Generic param was named `Tag` in the handler signature
(`tag: Tag`) but the imported type was aliased as `TagType`. Tag
was undefined → "Cannot find name 'Tag'". Renamed to TagType.
TagStrip.svelte
`dropAccepts?: string[]` is too wide for `passiveDropZone`'s
`accepts: DragType[]`. Narrowed the prop type to `DragType[]`
and added the missing import.
shared-auth/types: UserData.{name,image}?
Two more optional fields for the public user shape. Both come
from the JWT user_metadata claim when the user has filled in
their profile during onboarding. Without these the
ProfileStep.svelte onboarding component couldn't read
`authStore.user?.name` / `?.image` without `as any`. Added
alongside `twoFactorEnabled` from the previous shared-auth
commit; same Optional rationale (guest tokens omit the claim).
Net: -10 type errors.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
325 lines
9.7 KiB
Svelte
325 lines
9.7 KiB
Svelte
<script lang="ts">
|
|
import { X, Copy, QrCode, Link, ArrowSquareOut, Lock, Clock } from '@mana/shared-icons';
|
|
import type { CreatedLink, CreateShortLinkOptions } from './types';
|
|
import { createShortLink, isSharedUloadReady, getBaseUrl } from './create-link';
|
|
import { getQrCodeUrl, getShortUrl, downloadQrCode } from './utils';
|
|
|
|
interface Props {
|
|
visible: boolean;
|
|
onClose: () => void;
|
|
url: string;
|
|
title?: string;
|
|
source: string;
|
|
description?: string;
|
|
onCreated?: (link: CreatedLink) => void;
|
|
}
|
|
|
|
let { visible, onClose, url, title = '', source, description = '', onCreated }: Props = $props();
|
|
|
|
let customCode = $state('');
|
|
let useCustomCode = $state(false);
|
|
let password = $state('');
|
|
let usePassword = $state(false);
|
|
let expiresAt = $state('');
|
|
let useExpiry = $state(false);
|
|
let createdLink = $state<CreatedLink | null>(null);
|
|
let creating = $state(false);
|
|
let error = $state('');
|
|
let copied = $state(false);
|
|
let showQr = $state(false);
|
|
let showAdvanced = $state(false);
|
|
|
|
function reset() {
|
|
customCode = '';
|
|
useCustomCode = false;
|
|
password = '';
|
|
usePassword = false;
|
|
expiresAt = '';
|
|
useExpiry = false;
|
|
createdLink = null;
|
|
creating = false;
|
|
error = '';
|
|
copied = false;
|
|
showQr = false;
|
|
showAdvanced = false;
|
|
}
|
|
|
|
function handleClose() {
|
|
reset();
|
|
onClose();
|
|
}
|
|
|
|
async function handleCreate() {
|
|
if (!isSharedUloadReady()) {
|
|
error = 'uLoad ist nicht initialisiert';
|
|
return;
|
|
}
|
|
|
|
creating = true;
|
|
error = '';
|
|
|
|
try {
|
|
const options: CreateShortLinkOptions = {
|
|
url,
|
|
title: title || undefined,
|
|
source,
|
|
description: description || undefined,
|
|
customCode: useCustomCode && customCode ? customCode : undefined,
|
|
password: usePassword && password ? password : undefined,
|
|
expiresAt: useExpiry && expiresAt ? new Date(expiresAt).toISOString() : undefined,
|
|
};
|
|
|
|
const link = await createShortLink(options);
|
|
createdLink = link;
|
|
onCreated?.(link);
|
|
} catch (e) {
|
|
error = e instanceof Error ? e.message : 'Fehler beim Erstellen';
|
|
} finally {
|
|
creating = false;
|
|
}
|
|
}
|
|
|
|
async function copyToClipboard(text: string) {
|
|
await navigator.clipboard.writeText(text);
|
|
copied = true;
|
|
setTimeout(() => (copied = false), 2000);
|
|
}
|
|
|
|
function handleKeydown(e: KeyboardEvent) {
|
|
if (e.key === 'Escape' && visible) {
|
|
handleClose();
|
|
}
|
|
}
|
|
|
|
function handleBackdropClick(e: MouseEvent) {
|
|
if (e.target === e.currentTarget) {
|
|
handleClose();
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<svelte:window onkeydown={handleKeydown} />
|
|
|
|
{#if visible}
|
|
<div
|
|
class="fixed inset-0 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
style="z-index: 9990;"
|
|
onclick={handleBackdropClick}
|
|
onkeydown={(e) => {
|
|
if (e.key === 'Escape') handleClose();
|
|
}}
|
|
role="presentation"
|
|
tabindex="-1"
|
|
>
|
|
<div
|
|
class="w-full max-w-md rounded-2xl border border-white/10 bg-gray-900 shadow-2xl"
|
|
role="dialog"
|
|
aria-modal="true"
|
|
tabindex="-1"
|
|
onclick={(e) => e.stopPropagation()}
|
|
onkeydown={(e) => e.stopPropagation()}
|
|
>
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between border-b border-white/10 px-5 py-4">
|
|
<div class="flex items-center gap-2.5">
|
|
<Link size={18} class="text-indigo-400" />
|
|
<h3 class="text-base font-semibold text-white">Kurzlink erstellen</h3>
|
|
</div>
|
|
<button
|
|
onclick={handleClose}
|
|
class="rounded-lg p-1.5 text-gray-400 hover:bg-white/10 hover:text-white transition-colors"
|
|
aria-label="Schliessen"
|
|
>
|
|
<X size={18} />
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-5">
|
|
{#if !createdLink}
|
|
<!-- Create State -->
|
|
<div class="space-y-4">
|
|
<!-- URL Preview -->
|
|
<div class="rounded-lg bg-white/5 px-3 py-2.5">
|
|
<p class="text-xs text-gray-400 mb-1">Ziel-URL</p>
|
|
<p class="text-sm text-white truncate">{url}</p>
|
|
</div>
|
|
|
|
{#if title}
|
|
<div>
|
|
<p class="block text-xs text-gray-400 mb-1">Titel</p>
|
|
<p class="text-sm text-white">{title}</p>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Custom Code Toggle -->
|
|
<div>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={useCustomCode}
|
|
class="rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
|
|
/>
|
|
<span class="text-sm text-gray-300">Eigenen Kurzcode verwenden</span>
|
|
</label>
|
|
|
|
{#if useCustomCode}
|
|
<div class="mt-2 flex items-center gap-2">
|
|
<span class="text-sm text-gray-500">ulo.ad/</span>
|
|
<input
|
|
type="text"
|
|
bind:value={customCode}
|
|
placeholder="mein-code"
|
|
class="flex-1 rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-gray-500 focus:border-indigo-500 focus:outline-none"
|
|
/>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Advanced Options Toggle -->
|
|
<button
|
|
type="button"
|
|
onclick={() => (showAdvanced = !showAdvanced)}
|
|
class="flex items-center gap-1.5 text-xs text-gray-400 hover:text-gray-300 transition-colors"
|
|
>
|
|
<span class="transition-transform {showAdvanced ? 'rotate-90' : ''}">▶</span>
|
|
Erweiterte Optionen
|
|
</button>
|
|
|
|
{#if showAdvanced}
|
|
<div class="space-y-3 rounded-lg bg-white/5 p-3">
|
|
<!-- Password Protection -->
|
|
<div>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={usePassword}
|
|
class="rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
|
|
/>
|
|
<Lock size={14} class="text-gray-400" />
|
|
<span class="text-sm text-gray-300">Passwortschutz</span>
|
|
</label>
|
|
{#if usePassword}
|
|
<input
|
|
type="text"
|
|
bind:value={password}
|
|
placeholder="Passwort eingeben"
|
|
class="mt-2 w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white placeholder-gray-500 focus:border-indigo-500 focus:outline-none"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Expiration -->
|
|
<div>
|
|
<label class="flex items-center gap-2 cursor-pointer">
|
|
<input
|
|
type="checkbox"
|
|
bind:checked={useExpiry}
|
|
class="rounded border-gray-600 bg-gray-800 text-indigo-500 focus:ring-indigo-500"
|
|
/>
|
|
<Clock size={14} class="text-gray-400" />
|
|
<span class="text-sm text-gray-300">Ablaufdatum</span>
|
|
</label>
|
|
{#if useExpiry}
|
|
<input
|
|
type="datetime-local"
|
|
bind:value={expiresAt}
|
|
class="mt-2 w-full rounded-lg border border-white/10 bg-white/5 px-3 py-2 text-sm text-white focus:border-indigo-500 focus:outline-none"
|
|
/>
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if error}
|
|
<p class="text-sm text-red-400">{error}</p>
|
|
{/if}
|
|
|
|
<button
|
|
onclick={handleCreate}
|
|
disabled={creating}
|
|
class="w-full rounded-lg bg-indigo-600 px-4 py-2.5 text-sm font-medium text-white hover:bg-indigo-500 disabled:opacity-50 transition-colors"
|
|
>
|
|
{creating ? 'Erstelle...' : 'Kurzlink erstellen'}
|
|
</button>
|
|
</div>
|
|
{:else}
|
|
<!-- Created State -->
|
|
<div class="space-y-4">
|
|
<div
|
|
class="rounded-lg bg-emerald-500/10 border border-emerald-500/20 px-4 py-3 text-center"
|
|
>
|
|
<p class="text-xs text-emerald-400 mb-1">Kurzlink erstellt</p>
|
|
<p class="font-mono text-lg font-semibold text-emerald-300">
|
|
{createdLink.shortUrl}
|
|
</p>
|
|
</div>
|
|
|
|
{#if showQr}
|
|
<div class="flex flex-col items-center gap-3">
|
|
<div class="rounded-xl bg-white p-3">
|
|
<img
|
|
src={getQrCodeUrl(createdLink.shortUrl, 200)}
|
|
alt="QR Code für {createdLink.shortCode}"
|
|
class="h-44 w-44"
|
|
/>
|
|
</div>
|
|
<button
|
|
onclick={() => downloadQrCode(createdLink!.shortCode, getBaseUrl())}
|
|
class="text-sm text-indigo-400 hover:text-indigo-300 transition-colors"
|
|
>
|
|
QR herunterladen
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
|
|
<!-- Actions -->
|
|
<div class="flex gap-2">
|
|
<button
|
|
onclick={() => copyToClipboard(createdLink!.shortUrl)}
|
|
class="flex flex-1 items-center justify-center gap-2 rounded-lg border border-white/10 px-3 py-2.5 text-sm font-medium text-white hover:bg-white/5 transition-colors"
|
|
>
|
|
<Copy size={16} />
|
|
{copied ? 'Kopiert!' : 'Kopieren'}
|
|
</button>
|
|
<button
|
|
onclick={() => (showQr = !showQr)}
|
|
class="flex items-center justify-center gap-2 rounded-lg border border-white/10 px-3 py-2.5 text-sm font-medium text-white hover:bg-white/5 transition-colors {showQr
|
|
? 'bg-white/10'
|
|
: ''}"
|
|
>
|
|
<QrCode size={16} />
|
|
QR
|
|
</button>
|
|
<a
|
|
href="/my/links"
|
|
target="_blank"
|
|
class="flex items-center justify-center gap-2 rounded-lg border border-white/10 px-3 py-2.5 text-sm font-medium text-white hover:bg-white/5 transition-colors"
|
|
title="In uLoad öffnen"
|
|
>
|
|
<ArrowSquareOut size={16} />
|
|
</a>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
</div>
|
|
|
|
<!-- Footer: Source badge + protection info -->
|
|
<div class="border-t border-white/10 px-5 py-3">
|
|
<div class="flex items-center gap-3 text-xs text-gray-500">
|
|
<span>via <span class="text-gray-400">{source}</span></span>
|
|
{#if createdLink && usePassword && password}
|
|
<span class="flex items-center gap-1 text-amber-500">
|
|
<Lock size={10} /> Passwortgeschützt
|
|
</span>
|
|
{/if}
|
|
{#if createdLink && useExpiry && expiresAt}
|
|
<span class="flex items-center gap-1 text-blue-400">
|
|
<Clock size={10} /> Läuft ab
|
|
</span>
|
|
{/if}
|
|
<span class="ml-auto">Sichtbar in uLoad</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|