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
+162
View File
@@ -0,0 +1,162 @@
<script lang="ts">
import { goto } from "$app/navigation";
import { page } from "$app/state";
import { api, ApiError } from "$lib/api";
import { trackEvent } from "$lib/analytics";
import { apiFormError, fieldDescribedBy, fieldInvalid, parseBodyErrorCodes } from "$lib/api-form-error";
import { i18n } from "$lib/i18n";
import { safeInternalNext } from "$lib/safe-next";
import {
Alert,
AlertDescription,
Button,
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
Input,
Label
} from "$lib/components/ui";
const LOGIN_FIELD_HINTS = {
// Prefer stable codes (locale-safe). English needles are last-resort only.
email: ["invalid_credentials", "email", "credentials"],
password: ["invalid_credentials", "password_not_set", "password", "credentials"]
} as const;
let email = $state("");
let password = $state("");
let error = $state("");
let fieldErrors = $state<Record<string, string>>({});
let loading = $state(false);
let needsPasswordInvite = $state(false);
const errorId = "login-form-error";
function apiErrorCode(err: unknown): string {
if (!(err instanceof ApiError)) return "";
return parseBodyErrorCodes(err.body)[0] ?? "";
}
async function onSubmit(event: Event) {
event.preventDefault();
error = "";
fieldErrors = {};
needsPasswordInvite = false;
loading = true;
try {
await api("/api/auth/login", {
method: "POST",
body: { email, password }
});
trackEvent("login", { method: "email" });
await goto(safeInternalNext(page.url.searchParams.get("next"), "/dashboard"));
} catch (err) {
if (apiErrorCode(err) === "password_not_set") {
needsPasswordInvite = true;
const result = apiFormError(err, i18n.t("auth.login.passwordNotSet"));
error = result.message;
fieldErrors = {};
return;
}
const result = apiFormError(err, i18n.t("auth.login.failed"), LOGIN_FIELD_HINTS);
error = result.message;
fieldErrors = result.fields;
} finally {
loading = false;
}
}
</script>
<Card>
<CardHeader>
<CardTitle level={1}>{i18n.t("auth.login.title")}</CardTitle>
<CardDescription>{i18n.t("auth.login.description")}</CardDescription>
</CardHeader>
<CardContent>
<form
method="post"
class="space-y-4"
onsubmit={onSubmit}
aria-busy={loading}
aria-describedby={error ? errorId : undefined}
>
{#if error}
<Alert variant="destructive">
<AlertDescription id={errorId}>{error}</AlertDescription>
</Alert>
{/if}
{#if needsPasswordInvite}
<div class="space-y-2 rounded-md border bg-muted/40 px-3 py-3 text-sm text-text-muted">
<p>
<strong class="text-text">{i18n.t("auth.login.setPasswordFirstTitle")}</strong>
{i18n.t("auth.login.setPasswordFirstBody", {
email: email || i18n.t("auth.login.yourEmail")
})}
</p>
<p>
{i18n.t("auth.login.platformAdminsReissue")}
<a href="/admin/users" class="font-medium text-link hover:underline"
>{i18n.t("auth.login.adminUsersLink")}</a
>.
</p>
<p>
<a href="/accept-invite" class="font-medium text-link hover:underline"
>{i18n.t("auth.login.haveToken")}</a
>
</p>
</div>
{/if}
<div class="space-y-2">
<Label for="email">{i18n.t("common.email")}</Label>
<Input
id="email"
type="email"
name="email"
autocomplete="email"
required
aria-invalid={fieldInvalid(fieldErrors, "email") ?? (error && !Object.keys(fieldErrors).length ? "true" : undefined)}
aria-describedby={fieldDescribedBy(fieldErrors, "email", errorId) ?? (error && !Object.keys(fieldErrors).length ? errorId : undefined)}
bind:value={email}
/>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between gap-2">
<Label for="password">{i18n.t("common.password")}</Label>
<a href="/forgot-password" class="text-xs font-medium text-link hover:underline">
{i18n.t("auth.login.forgotPassword")}
</a>
</div>
<Input
id="password"
type="password"
name="password"
autocomplete="current-password"
required
aria-invalid={fieldInvalid(fieldErrors, "password") ?? (error && !Object.keys(fieldErrors).length ? "true" : undefined)}
aria-describedby={fieldDescribedBy(fieldErrors, "password", errorId) ?? (error && !Object.keys(fieldErrors).length ? errorId : undefined)}
bind:value={password}
/>
</div>
<Button type="submit" class="w-full shadow-sm" loading={loading}>
{loading ? i18n.t("auth.login.submitting") : i18n.t("auth.login.submit")}
</Button>
</form>
<p class="mt-5 text-center text-sm text-text-muted">
{i18n.t("auth.login.noAccount")}
<a href="/register" class="font-medium text-link hover:underline">{i18n.t("auth.login.createCompany")}</a>
</p>
<p class="mt-1.5 text-center text-sm text-text-muted">
{i18n.t("auth.login.haveInvite")}
<a href="/accept-invite" class="font-medium text-link hover:underline">{i18n.t("auth.login.acceptInvite")}</a>
</p>
<p class="mt-1.5 text-center text-sm text-text-muted">
<a href="/pricing" class="font-medium text-link hover:underline">{i18n.t("common.viewPricing")}</a>
</p>
</CardContent>
</Card>