mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 19:51:17 +00:00
159 lines
7.8 KiB
TypeScript
159 lines
7.8 KiB
TypeScript
"use client"
|
||
|
||
import { useRouter } from "next/navigation"
|
||
import { useState, useSyncExternalStore, useTransition } from "react"
|
||
import { CopyPlusIcon, ExternalLinkIcon, RefreshCwIcon, Trash2Icon } from "lucide-react"
|
||
import { Button, buttonVariants } from "@/components/ui/button"
|
||
import { CopyField } from "@/modules/admin/ui/components/copy-field"
|
||
import { deleteVariant, duplicateVariant, updateShare } from "../../../actions/variants"
|
||
import { dateFormat, SHARE_EXPIRY_OPTIONS } from "../../../lib/preview"
|
||
import type { EditorVariant } from "../../views/variant-editor"
|
||
|
||
const selectClass = "h-10 w-full rounded-md border bg-background px-3 text-sm outline-none focus-visible:ring-3 focus-visible:ring-ring/50"
|
||
const subscribe = () => () => {}
|
||
|
||
function Heading({ title, text }: { title: string; text?: string }) {
|
||
return (
|
||
<div className="mb-3">
|
||
<h3 className="text-sm font-medium">{title}</h3>
|
||
{text && <p className="mt-0.5 text-xs text-muted-foreground">{text}</p>}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
export function SharePanel({ variant, clients, clientId, onClientChange, dirty }: {
|
||
variant: EditorVariant
|
||
clients: { id: string; name: string }[]
|
||
clientId: string | null
|
||
onClientChange: (clientId: string | null) => void
|
||
dirty: boolean
|
||
}) {
|
||
const router = useRouter()
|
||
const origin = useSyncExternalStore(subscribe, () => window.location.origin, () => "")
|
||
const [share, setShare] = useState({ token: variant.shareToken, expiresAt: variant.shareExpiresAt, expired: variant.shareState === "expired" })
|
||
// -1 = bestehendes Ablaufdatum beibehalten
|
||
const [expiry, setExpiry] = useState<number>(variant.shareExpiresAt ? -1 : 0)
|
||
const [confirmDelete, setConfirmDelete] = useState(false)
|
||
const [error, setError] = useState<string | null>(null)
|
||
const [pending, startTransition] = useTransition()
|
||
|
||
function run(action: () => Promise<{ error: string } | unknown>) {
|
||
startTransition(async () => {
|
||
const result = await action()
|
||
if (result && typeof result === "object" && "error" in result) setError(String(result.error))
|
||
else setError(null)
|
||
})
|
||
}
|
||
|
||
function applyShare(enabled: boolean, options: { regenerate?: boolean; days?: number } = {}) {
|
||
run(async () => {
|
||
const days = options.days ?? (expiry < 0 ? 0 : expiry)
|
||
const result = await updateShare(variant.id, { enabled, expiresInDays: days, regenerate: options.regenerate })
|
||
if (!("error" in result)) {
|
||
setShare({ ...result, expired: false })
|
||
setExpiry(result.expiresAt ? days : 0)
|
||
}
|
||
return result
|
||
})
|
||
}
|
||
|
||
const url = share.token ? `${origin}/demo/${share.token}` : ""
|
||
|
||
return (
|
||
<div className="flex flex-col gap-8">
|
||
<section>
|
||
<Heading title="Client" text="Optional – ordnet die Variante einem Kunden zu. Wird mit „Speichern“ übernommen." />
|
||
<select
|
||
value={clientId ?? ""}
|
||
onChange={(event) => onClientChange(event.target.value || null)}
|
||
className={selectClass}
|
||
aria-label="Client"
|
||
>
|
||
<option value="">Kein Client (Pitch)</option>
|
||
{clients.map((client) => (
|
||
<option key={client.id} value={client.id}>{client.name}</option>
|
||
))}
|
||
</select>
|
||
</section>
|
||
|
||
<section>
|
||
<Heading title="Link teilen" text="Kunden öffnen den Link ohne Login. Er zeigt immer den zuletzt gespeicherten Stand." />
|
||
{share.token ? (
|
||
<div className="flex flex-col gap-3">
|
||
{share.expired && (
|
||
<p className="rounded-md bg-signal-warning/12 px-3 py-2 text-xs text-signal-warning">
|
||
Dieser Link ist abgelaufen. Wähle eine neue Gültigkeit, um ihn wieder zu aktivieren.
|
||
</p>
|
||
)}
|
||
<CopyField label="Öffentlicher Link" value={url} />
|
||
<label className="flex flex-col gap-1.5 text-sm">
|
||
Gültigkeit
|
||
<select
|
||
value={expiry}
|
||
disabled={pending}
|
||
onChange={(event) => applyShare(true, { days: Number(event.target.value) })}
|
||
className={selectClass}
|
||
>
|
||
{expiry < 0 && share.expiresAt && (
|
||
<option value={-1}>bis {dateFormat.format(new Date(share.expiresAt))}</option>
|
||
)}
|
||
{SHARE_EXPIRY_OPTIONS.map((option) => (
|
||
<option key={option.days} value={option.days}>{option.label}</option>
|
||
))}
|
||
</select>
|
||
</label>
|
||
<div className="flex flex-wrap gap-2">
|
||
<a href={url} target="_blank" rel="noreferrer" className={buttonVariants({ variant: "outline", size: "sm" })}>
|
||
<ExternalLinkIcon /> Öffnen
|
||
</a>
|
||
<Button variant="outline" size="sm" disabled={pending} onClick={() => applyShare(true, { regenerate: true })}>
|
||
<RefreshCwIcon /> Neuer Link
|
||
</Button>
|
||
<Button variant="ghost" size="sm" disabled={pending} onClick={() => applyShare(false)}>
|
||
Freigabe beenden
|
||
</Button>
|
||
</div>
|
||
<p className="text-xs text-muted-foreground">„Neuer Link“ macht den bisherigen Link ungültig.</p>
|
||
</div>
|
||
) : (
|
||
<Button disabled={pending} onClick={() => applyShare(true)}>Link erstellen</Button>
|
||
)}
|
||
</section>
|
||
|
||
<section>
|
||
<Heading title="Variante" />
|
||
<div className="flex flex-col gap-2">
|
||
<Button variant="outline" disabled={pending} onClick={() => run(() => duplicateVariant(variant.id))}>
|
||
<CopyPlusIcon /> Duplizieren
|
||
</Button>
|
||
{dirty && <p className="text-xs text-muted-foreground">Dupliziert wird der gespeicherte Stand.</p>}
|
||
{confirmDelete ? (
|
||
<div className="flex gap-2">
|
||
<Button
|
||
variant="destructive"
|
||
className="flex-1"
|
||
disabled={pending}
|
||
onClick={() =>
|
||
run(async () => {
|
||
const result = await deleteVariant(variant.id)
|
||
if ("success" in result) router.push("/admin/templates")
|
||
return result
|
||
})
|
||
}
|
||
>
|
||
Endgültig löschen
|
||
</Button>
|
||
<Button variant="outline" onClick={() => setConfirmDelete(false)}>Abbrechen</Button>
|
||
</div>
|
||
) : (
|
||
<Button variant="ghost" className="text-destructive" onClick={() => setConfirmDelete(true)}>
|
||
<Trash2Icon /> Löschen
|
||
</Button>
|
||
)}
|
||
</div>
|
||
</section>
|
||
|
||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||
</div>
|
||
)
|
||
}
|