feat(tenant): make connect panel dismissible after connecting
All checks were successful
Build and Push / build (push) Successful in 1m49s

This commit is contained in:
2026-05-29 23:55:53 +02:00
parent c1833c1def
commit 73f1af185f
6 changed files with 104 additions and 10 deletions

View File

@@ -225,6 +225,7 @@ export default async function TenantDetailPage({
unreachable). */}
<section className="mb-8 animate-in animate-in-delay-1">
<ConnectPanel
tenantName={name}
enabledChannels={enabledChannels}
phase={tenant.status?.phase ?? "Pending"}
/>

View File

@@ -1,5 +1,6 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
@@ -16,11 +17,16 @@ import { THREEMA_GATEWAY } from "@/lib/threema-gateway-config";
* NO channel is enabled it says so explicitly (a running assistant with
* no channel is unreachable).
*
* Once a customer has connected they don't need the steps every visit,
* so the panel is dismissible: clicking "I've connected" collapses it
* to a slim row and remembers that per-tenant (localStorage). The slim
* row keeps a "Show connection details" toggle so it's never lost.
* The no-channel warning is NOT dismissible — it's an actionable alert,
* not reference material.
*
* It is intentionally complementary to ChannelUsers below it:
* - ConnectPanel → "how do *I* reach the assistant"
* - ChannelUsers → "*who* is allowed to reach it"
* The Threema/Telegram/Discord steps reference the authorised-users
* list rather than duplicating it.
*/
// Render order is fixed (not the order packages happen to appear in
@@ -40,10 +46,15 @@ const CHANNEL_STEPS_KEY: Record<string, string> = {
discord: "discordSteps",
};
const dismissKey = (tenantName: string) =>
`pieced:connect-hidden:${tenantName}`;
export function ConnectPanel({
tenantName,
enabledChannels,
phase,
}: {
tenantName: string;
enabledChannels: string[];
/** Tenant phase — connection details only "work" once it's Ready. */
phase: string;
@@ -53,7 +64,41 @@ export function ConnectPanel({
const channels = CHANNEL_ORDER.filter((c) => enabledChannels.includes(c));
const ready = phase === "Ready" || phase === "Running" || phase === "Active";
// No channel at all → the assistant is unreachable. Make it loud.
// Dismissed state is read from localStorage after mount to avoid a
// hydration mismatch (server has no localStorage). `hydrated` gates
// the collapsed view so the first paint matches the server output.
const [collapsed, setCollapsed] = useState(false);
const [hydrated, setHydrated] = useState(false);
useEffect(() => {
try {
setCollapsed(localStorage.getItem(dismissKey(tenantName)) === "1");
} catch {
/* private mode / storage disabled — just stay expanded */
}
setHydrated(true);
}, [tenantName]);
const dismiss = () => {
setCollapsed(true);
try {
localStorage.setItem(dismissKey(tenantName), "1");
} catch {
/* no-op */
}
};
const reopen = () => {
setCollapsed(false);
try {
localStorage.removeItem(dismissKey(tenantName));
} catch {
/* no-op */
}
};
// No channel at all → the assistant is unreachable. Make it loud and
// keep it non-dismissible (it's an alert, not reference material).
if (channels.length === 0) {
return (
<div className="rounded-xl border border-amber-500/30 bg-amber-500/10 p-5">
@@ -85,11 +130,51 @@ export function ConnectPanel({
);
}
// Collapsed: a slim, unobtrusive row with a toggle to bring the full
// panel back. Only shown once hydrated so SSR/CSR agree.
if (hydrated && collapsed) {
return (
<div className="flex items-center justify-between rounded-lg border border-border bg-surface-1 px-4 py-2">
<span className="text-xs text-text-muted">{t("title")}</span>
<button
type="button"
onClick={reopen}
className="text-xs font-medium text-accent hover:text-accent-dim transition-colors cursor-pointer"
>
{t("show")}
</button>
</div>
);
}
return (
<div className="rounded-xl border border-accent/30 bg-accent/5 p-5">
<h2 className="font-display text-base font-semibold text-text-primary mb-1">
{t("title")}
</h2>
<div className="flex items-start justify-between gap-3 mb-1">
<h2 className="font-display text-base font-semibold text-text-primary">
{t("title")}
</h2>
<button
type="button"
onClick={dismiss}
className="shrink-0 inline-flex items-center gap-1 text-xs font-medium text-text-muted hover:text-text-secondary transition-colors cursor-pointer"
>
<svg
className="h-3.5 w-3.5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M4.5 12.75l6 6 9-13.5"
/>
</svg>
{t("dismiss")}
</button>
</div>
<p className="text-xs text-text-secondary mb-4 leading-relaxed">
{t("description")}
</p>

View File

@@ -1005,6 +1005,8 @@
"threemaBotIdLabel": "Threema-ID",
"threemaSteps": "1. Öffnen Sie Threema und scannen Sie diesen QR-Code (oder fügen Sie die obige ID als Kontakt hinzu).\n2. Senden Sie eine Nachricht, um den Chat zu starten.\nStellen Sie sicher, dass Ihre eigene Threema-ID in der Liste der autorisierten Benutzer unten steht nur gelistete IDs erhalten eine Antwort.",
"telegramSteps": "Öffnen Sie den verbundenen Telegram-Bot und senden Sie ihm eine Nachricht, um den Chat zu starten. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort.",
"discordSteps": "Schreiben Sie dem verbundenen Discord-Bot oder erwähnen Sie ihn in einem Kanal, dem er beigetreten ist. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort."
"discordSteps": "Schreiben Sie dem verbundenen Discord-Bot oder erwähnen Sie ihn in einem Kanal, dem er beigetreten ist. Nur die Benutzer-IDs in der Liste der autorisierten Benutzer unten erhalten eine Antwort.",
"dismiss": "Verbunden",
"show": "Verbindungsdetails anzeigen"
}
}

View File

@@ -1005,6 +1005,8 @@
"threemaBotIdLabel": "Threema ID",
"threemaSteps": "1. Open Threema and scan this QR code (or add the ID above as a contact).\n2. Send it a message to start chatting.\nMake sure your own Threema ID is on the authorised users list below — only listed IDs get a reply.",
"telegramSteps": "Open the Telegram bot you connected and send it a message to start chatting. Only the user IDs on the authorised users list below get a reply.",
"discordSteps": "Message the Discord bot you connected, or mention it in a channel it has joined. Only the user IDs on the authorised users list below get a reply."
"discordSteps": "Message the Discord bot you connected, or mention it in a channel it has joined. Only the user IDs on the authorised users list below get a reply.",
"dismiss": "I've connected",
"show": "Show connection details"
}
}

View File

@@ -1005,6 +1005,8 @@
"threemaBotIdLabel": "Identifiant Threema",
"threemaSteps": "1. Ouvrez Threema et scannez ce QR code (ou ajoutez l'identifiant ci-dessus comme contact).\n2. Envoyez-lui un message pour commencer à discuter.\nAssurez-vous que votre propre identifiant Threema figure dans la liste des utilisateurs autorisés ci-dessous — seuls les identifiants listés reçoivent une réponse.",
"telegramSteps": "Ouvrez le bot Telegram que vous avez connecté et envoyez-lui un message pour commencer à discuter. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse.",
"discordSteps": "Écrivez au bot Discord que vous avez connecté, ou mentionnez-le dans un salon qu'il a rejoint. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse."
"discordSteps": "Écrivez au bot Discord que vous avez connecté, ou mentionnez-le dans un salon qu'il a rejoint. Seuls les identifiants utilisateur de la liste des utilisateurs autorisés ci-dessous reçoivent une réponse.",
"dismiss": "Je suis connecté",
"show": "Afficher les détails de connexion"
}
}

View File

@@ -1005,6 +1005,8 @@
"threemaBotIdLabel": "ID Threema",
"threemaSteps": "1. Apri Threema e scansiona questo codice QR (oppure aggiungi l'ID sopra come contatto).\n2. Inviagli un messaggio per iniziare a chattare.\nAssicurati che il tuo ID Threema sia presente nell'elenco degli utenti autorizzati qui sotto: solo gli ID elencati ricevono una risposta.",
"telegramSteps": "Apri il bot Telegram che hai collegato e inviagli un messaggio per iniziare a chattare. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta.",
"discordSteps": "Scrivi al bot Discord che hai collegato, oppure menzionalo in un canale a cui si è unito. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta."
"discordSteps": "Scrivi al bot Discord che hai collegato, oppure menzionalo in un canale a cui si è unito. Solo gli ID utente nell'elenco degli utenti autorizzati qui sotto ricevono una risposta.",
"dismiss": "Mi sono collegato",
"show": "Mostra dettagli di connessione"
}
}