174 lines
8.1 KiB
TypeScript
174 lines
8.1 KiB
TypeScript
import { useMemo, useState } from "react";
|
|
import { Form, useActionData, useLoaderData, useNavigate, useNavigation } from "react-router";
|
|
import { useTranslation } from "react-i18next";
|
|
import {
|
|
FeatherChevronRight,
|
|
FeatherPlus,
|
|
FeatherUserPlus,
|
|
FeatherUsers,
|
|
} from "@subframe/core";
|
|
import * as SubframeCore from "@subframe/core";
|
|
import { useRootData } from "~/utils/data/useRootData";
|
|
import { Avatar } from "~/ui/components/Avatar";
|
|
import { Button } from "~/ui/components/Button";
|
|
import { Card } from "~/ui/components/Card";
|
|
import { DropdownMenu } from "~/ui/components/DropdownMenu";
|
|
import { ErrorState } from "~/ui/components/ErrorState";
|
|
import { Logo } from "~/ui/components/Logo";
|
|
import { SearchInput } from "~/ui/components/SearchInput";
|
|
import { Tag_Label } from "~/ui/components/Tag_Label";
|
|
import type { ActionData, LoaderData } from "~/routeModules/auth/select-tenant.impl";
|
|
|
|
// D-D0a — Flow "Login Tenantauswahl (Admin)". 1:1 an Davids Subframe-Page "Tenantauswahl": Logo +
|
|
// Karten-Box (Such-Feld immer sichtbar, Profil-Karten, Divider, "Profil hinzufuegen"-Dropdown). An die
|
|
// echten Tenants des Users (Loader-VM) verdrahtet: Karte = Form-Submit -> Action (CSRF + Membership-
|
|
// Check serverseitig) -> Tenant-Space. SearchInput filtert client-seitig. Fehler/Empty via ~/ui ErrorState.
|
|
// data-ui-id-Marker bleiben beim spaeteren Subframe-Swap erhalten.
|
|
|
|
export default function SelectTenantRoute() {
|
|
const { t } = useTranslation();
|
|
const { csrf } = useRootData();
|
|
const { vm } = useLoaderData<LoaderData>();
|
|
const actionData = useActionData<ActionData>();
|
|
const navigate = useNavigate();
|
|
const navigation = useNavigation();
|
|
const isSubmitting = navigation.state !== "idle";
|
|
const [query, setQuery] = useState("");
|
|
|
|
const tenants = vm.status === "ready" ? vm.data.tenants : [];
|
|
const visibleTenants = useMemo(() => {
|
|
const q = query.trim().toLowerCase();
|
|
if (!q) return tenants;
|
|
return tenants.filter((tenant) => tenant.name.toLowerCase().includes(q));
|
|
}, [tenants, query]);
|
|
|
|
return (
|
|
<div
|
|
className="flex min-h-screen w-full flex-col items-center justify-center gap-8 bg-surface-neutral-primary px-4 py-12"
|
|
data-ui-id="ui.auth.selectTenant.page"
|
|
>
|
|
<Logo size="64" />
|
|
<div className="flex w-full max-w-[36rem] flex-col items-start gap-6 rounded-lg border border-solid border-border-default bg-surface-neutral-secondary px-8 py-8 shadow-lg">
|
|
<div className="flex w-full flex-col items-center gap-2">
|
|
<h1 className="text-heading- font-heading- text-text-neutral-primary">{t("account.selectTenant.title")}</h1>
|
|
<p className="w-full text-center text-body-small-regular-150 font-body-small-regular-150 text-text-neutral-tertiary">
|
|
{t("account.selectTenant.subtitle")}
|
|
</p>
|
|
</div>
|
|
|
|
{actionData?.error ? (
|
|
<ErrorState
|
|
variant="full-width"
|
|
hideCancel
|
|
title={t(actionData.error)}
|
|
data-ui-id="ui.auth.selectTenant.error"
|
|
/>
|
|
) : null}
|
|
|
|
{vm.status === "ready" ? (
|
|
<SearchInput
|
|
className="h-auto w-full min-w-[288px] flex-none"
|
|
fillContainer
|
|
hideShortcut
|
|
placeholder={t("account.selectTenant.searchPlaceholder")}
|
|
value={query}
|
|
onChange={(event) => setQuery(event.target.value)}
|
|
aria-label={t("account.selectTenant.searchPlaceholder")}
|
|
data-ui-id="ui.auth.selectTenant.search"
|
|
/>
|
|
) : null}
|
|
|
|
{vm.status === "empty" ? (
|
|
<div className="flex w-full flex-col items-center gap-4 py-4 text-center" data-ui-id="ui.auth.selectTenant.empty">
|
|
<p className="text-body-default-regular-0 font-body-default-regular-0 text-text-neutral-secondary">
|
|
{t("account.selectTenant.empty")}
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div className="flex w-full flex-col items-start gap-2" data-ui-id="ui.auth.selectTenant.list">
|
|
{visibleTenants.length === 0 ? (
|
|
<p className="w-full py-4 text-center text-body-small-regular-150 font-body-small-regular-150 text-text-neutral-tertiary">
|
|
{t("account.selectTenant.noResults")}
|
|
</p>
|
|
) : (
|
|
visibleTenants.map((tenant) => (
|
|
<Form method="post" key={tenant.id} className="w-full">
|
|
<input type="hidden" name="csrf" value={csrf ?? ""} readOnly />
|
|
<input type="hidden" name="tenantId" value={tenant.id} readOnly />
|
|
<input type="hidden" name="slug" value={tenant.slug} readOnly />
|
|
<button
|
|
type="submit"
|
|
disabled={isSubmitting}
|
|
className="w-full text-left transition disabled:cursor-default disabled:opacity-60"
|
|
data-ui-id="ui.auth.selectTenant.card"
|
|
>
|
|
<Card className="group/card transition hover:border-border-strong hover:bg-surface-neutral-tertiary">
|
|
<div className="flex w-full items-center gap-4">
|
|
<Avatar size="medium" color={tenant.avatarColor} image={tenant.avatarImage}>
|
|
{tenant.avatarInitials}
|
|
</Avatar>
|
|
<div className="flex grow shrink-0 basis-0 flex-col items-start gap-1">
|
|
<div className="flex w-full items-center gap-2">
|
|
<span className="text-body-default-medium-0 font-body-default-medium-0 text-text-neutral-primary">
|
|
{tenant.name}
|
|
</span>
|
|
{tenant.role && tenant.roleTone ? (
|
|
<Tag_Label tone={tenant.roleTone} noAction size="small">
|
|
{t(tenant.roleLabelKey ?? "")}
|
|
</Tag_Label>
|
|
) : null}
|
|
</div>
|
|
<span className="text-body-extra-small-regular-150 font-body-extra-small-regular-150 text-text-neutral-tertiary">
|
|
{[tenant.subtitle, tenant.isCurrent ? t("account.selectTenant.lastUsed") : null]
|
|
.filter(Boolean)
|
|
.join(" · ")}
|
|
</span>
|
|
</div>
|
|
<FeatherChevronRight className="text-body-large font-body-large text-icon-neutral-tertiary group-hover/card:text-icon-neutral-primary" />
|
|
</div>
|
|
</Card>
|
|
</button>
|
|
</Form>
|
|
))
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex h-px w-full flex-none flex-col items-center bg-border-default" />
|
|
|
|
<SubframeCore.DropdownMenu.Root>
|
|
<SubframeCore.DropdownMenu.Trigger asChild>
|
|
<Button
|
|
className="h-auto w-full flex-none"
|
|
icon={<FeatherPlus />}
|
|
data-ui-id="ui.auth.selectTenant.addProfile"
|
|
>
|
|
{t("account.selectTenant.addProfile")}
|
|
</Button>
|
|
</SubframeCore.DropdownMenu.Trigger>
|
|
<SubframeCore.DropdownMenu.Portal>
|
|
<SubframeCore.DropdownMenu.Content side="bottom" align="center" sideOffset={4} asChild>
|
|
<DropdownMenu className="h-auto w-auto flex-none">
|
|
<DropdownMenu.DropdownItem
|
|
icon={<FeatherUsers />}
|
|
onClick={() => navigate("/login")}
|
|
data-ui-id="ui.auth.selectTenant.addExisting"
|
|
>
|
|
{t("account.selectTenant.addExisting")}
|
|
</DropdownMenu.DropdownItem>
|
|
<DropdownMenu.DropdownItem
|
|
icon={<FeatherUserPlus />}
|
|
onClick={() => navigate("/new-account")}
|
|
data-ui-id="ui.auth.selectTenant.addNew"
|
|
>
|
|
{t("account.selectTenant.addNew")}
|
|
</DropdownMenu.DropdownItem>
|
|
</DropdownMenu>
|
|
</SubframeCore.DropdownMenu.Content>
|
|
</SubframeCore.DropdownMenu.Portal>
|
|
</SubframeCore.DropdownMenu.Root>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|