Initial commit of Descrybe v2 without local scratch artifacts.

Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
This commit is contained in:
2026-08-09 22:47:43 +02:00
commit 8580c996c3
1285 changed files with 325780 additions and 0 deletions
@@ -0,0 +1,191 @@
<script lang="ts">
import { onMount } from "svelte";
import { goto } from "$app/navigation";
import { CalendarDays, Megaphone, Sparkles, RefreshCw } from "@lucide/svelte";
import { api, ApiError, failureMessage } from "$lib/api";
import { notifySuccess, notifyApiError } from "$lib/notify";
import PageShell from "$lib/components/PageShell.svelte";
import Alert from "$lib/components/Alert.svelte";
import Spinner from "$lib/components/Spinner.svelte";
import EmptyState from "$lib/components/EmptyState.svelte";
import { i18n } from "$lib/i18n";
import {
Badge,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle
} from "$lib/components/ui";
type PresetId = "black_friday" | "christmas";
type Preset = {
id: PresetId;
name: string;
description: string;
start_date: string;
end_date: string;
year: number;
};
type Prepared = {
preset_id: PresetId;
name: string;
start_date: string;
end_date: string;
year: number;
export_feed_id: string;
export_feed_name: string;
};
const year = new Date().getUTCFullYear();
let presets = $state<Preset[]>([]);
let prepared = $state<Prepared[]>([]);
let loading = $state(true);
let error = $state("");
let pendingPreset = $state<PresetId | null>(null);
const preparedByKey = $derived(
new Map(prepared.map((p) => [`${p.preset_id}:${p.year}`, p] as const))
);
async function load() {
loading = true;
error = "";
try {
const res = await api<{ year: number; presets: Preset[]; prepared: Prepared[] }>(
`/api/marketing/calendar?year=${year}`
);
presets = res.presets ?? [];
prepared = res.prepared ?? [];
} catch (err) {
if (err instanceof ApiError && (err.status === 401 || err.status === 403)) {
await goto("/login");
return;
}
if (err instanceof ApiError && (err.status === 404 || err.status === 503)) {
presets = [];
prepared = [];
error = "";
} else {
error = failureMessage(err, i18n.t("marketing.loadFailed"));
}
} finally {
loading = false;
}
}
async function prepare(presetId: PresetId) {
pendingPreset = presetId;
try {
const campaign = await api<Prepared & { created?: boolean }>(
"/api/marketing/calendar/prepare",
{
method: "POST",
body: { preset_id: presetId, year, format: "csv" }
}
);
notifySuccess(
campaign.created ? i18n.t("marketing.prepared") : i18n.t("marketing.alreadyReady"),
i18n.t("marketing.exportFeedsHint", { name: campaign.export_feed_name })
);
await load();
if (campaign.created && campaign.export_feed_id) {
await goto(`/export-feeds?highlight=${encodeURIComponent(campaign.export_feed_id)}`);
}
} catch (err) {
notifyApiError(err, i18n.t("marketing.requestFailed"), {
title: i18n.t("marketing.prepareFailed")
});
} finally {
pendingPreset = null;
}
}
onMount(() => {
void load();
});
</script>
<PageShell title={i18n.t("marketing.calendarTitle")} description={i18n.t("marketing.calendarDesc")}>
{#snippet actions()}
<Button variant="outline" onclick={() => void load()} disabled={loading}>
<RefreshCw class="mr-2 h-4 w-4" />
{i18n.t("common.refresh")}
</Button>
{/snippet}
{#if loading}
<div class="flex justify-center py-16">
<Spinner />
</div>
{:else if error}
<Alert message={error} />
{:else if presets.length === 0}
<EmptyState title={i18n.t("marketing.emptyTitle")} message={i18n.t("marketing.emptyMessage")}>
<a href="/campaigns"><Button variant="outline">{i18n.t("marketing.openCampaigns")}</Button></a>
</EmptyState>
{:else}
<div class="grid gap-4 md:grid-cols-2">
{#each presets as preset (preset.id)}
{@const existing = preparedByKey.get(`${preset.id}:${preset.year}`)}
{@const busy = pendingPreset === preset.id}
<Card>
<CardHeader>
<div class="flex items-start justify-between gap-2">
<div>
<CardTitle class="flex items-center gap-2">
<Megaphone class="h-4 w-4" />
{preset.name}
</CardTitle>
<CardDescription class="mt-2">{preset.description}</CardDescription>
</div>
<Badge variant="outline">{year}</Badge>
</div>
</CardHeader>
<CardContent class="space-y-4">
<div class="text-sm">
<span class="text-muted-foreground">{i18n.t("marketing.window")}</span>
<span class="ml-1 font-medium">{preset.start_date} → {preset.end_date}</span>
</div>
{#if existing}
<div class="rounded-md border bg-muted/40 p-3 text-sm">
{i18n.t("marketing.linkedExport")}
<button
type="button"
class="ml-1 font-medium underline"
onclick={() => void goto(`/export-feeds`)}
>
{existing.export_feed_name}
</button>
</div>
<Button class="w-full" onclick={() => void goto("/export-feeds")}>
{i18n.t("marketing.openPrepared")}
</Button>
{:else}
<Button
class="w-full"
disabled={pendingPreset !== null}
onclick={() => void prepare(preset.id)}
>
<Sparkles class="mr-2 h-4 w-4" />
{busy ? i18n.t("marketing.preparing") : i18n.t("marketing.prepareNamed", { name: preset.name })}
</Button>
{/if}
</CardContent>
</Card>
{/each}
</div>
<p class="mt-6 flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
<CalendarDays class="h-4 w-4" />
<span>
{i18n.t("marketing.footerBefore")}
<a class="underline" href="/campaigns">/campaigns</a>
{i18n.t("marketing.footerAfter")}
</span>
</p>
{/if}
</PageShell>