mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 21:41:16 +00:00
mobile responsive
This commit is contained in:
parent
496ad28723
commit
b6f741be8f
30 changed files with 245 additions and 159 deletions
|
|
@ -183,7 +183,7 @@ function BillingDialogContent({
|
|||
<DialogHeader>
|
||||
<DialogTitle>Vorschau</DialogTitle>
|
||||
</DialogHeader>
|
||||
<iframe src={previewUrl} title="Rechnungsvorschau" className="h-[70vh] w-full rounded-md border border-border" />
|
||||
<iframe src={previewUrl} title="Rechnungsvorschau" className="h-[60dvh] w-full rounded-md border border-border sm:h-[70dvh]" />
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
<DialogFooter className="gap-2">
|
||||
<Button type="button" variant="outline" onClick={() => setPreviewUrl(null)} disabled={createPending}>
|
||||
|
|
@ -215,7 +215,7 @@ function BillingDialogContent({
|
|||
<FieldLabel htmlFor="billingStreet">Straße <span className="text-destructive">*</span></FieldLabel>
|
||||
<Input id="billingStreet" value={header.billingStreet} onChange={(e) => updateHeader("billingStreet", e.target.value)} />
|
||||
</Field>
|
||||
<div className="grid grid-cols-[1fr_2fr_4rem] gap-2">
|
||||
<div className="grid grid-cols-[minmax(0,1fr)_minmax(0,2fr)_4rem] gap-2">
|
||||
<Field>
|
||||
<FieldLabel htmlFor="billingPostalCode">PLZ <span className="text-destructive">*</span></FieldLabel>
|
||||
<Input id="billingPostalCode" value={header.billingPostalCode} onChange={(e) => updateHeader("billingPostalCode", e.target.value)} />
|
||||
|
|
@ -256,8 +256,8 @@ function BillingDialogContent({
|
|||
const net = Number.isFinite(item.quantity) && Number.isFinite(item.unitPrice) ? lineNet(item) : null;
|
||||
|
||||
return (
|
||||
<div key={row.key} className="grid gap-2 rounded-lg border border-border p-3 md:grid-cols-[10rem_1fr_5rem_7rem_5rem_auto] md:items-end">
|
||||
<Field>
|
||||
<div key={row.key} className="grid grid-cols-3 items-end gap-2 rounded-lg border border-border p-3 md:grid-cols-[10rem_1fr_5rem_7rem_5rem_auto]">
|
||||
<Field className="col-span-3 md:col-span-1">
|
||||
<FieldLabel htmlFor={`template-${row.key}`}>Vorlage</FieldLabel>
|
||||
<select
|
||||
id={`template-${row.key}`}
|
||||
|
|
@ -274,7 +274,7 @@ function BillingDialogContent({
|
|||
))}
|
||||
</select>
|
||||
</Field>
|
||||
<Field>
|
||||
<Field className="col-span-3 md:col-span-1">
|
||||
<FieldLabel htmlFor={`description-${row.key}`}>Bezeichnung</FieldLabel>
|
||||
<Input id={`description-${row.key}`} value={row.description} onChange={(e) => updateRow(row.key, { description: e.target.value })} />
|
||||
</Field>
|
||||
|
|
@ -290,7 +290,7 @@ function BillingDialogContent({
|
|||
<FieldLabel htmlFor={`tax-${row.key}`}>MwSt %</FieldLabel>
|
||||
<Input id={`tax-${row.key}`} inputMode="decimal" value={row.taxRate} onChange={(e) => updateRow(row.key, { taxRate: e.target.value })} />
|
||||
</Field>
|
||||
<div className="flex items-center justify-between gap-2 md:justify-end md:h-10">
|
||||
<div className="col-span-3 flex items-center justify-between gap-2 md:col-span-1 md:h-10 md:justify-end">
|
||||
<span className="min-w-20 text-right tabular-nums text-muted-foreground">
|
||||
{net === null ? "–" : formatCurrency(net)}
|
||||
</span>
|
||||
|
|
@ -363,7 +363,7 @@ export function BillingDialog({
|
|||
</>
|
||||
)}
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-h-[90vh] overflow-y-auto sm:max-w-5xl">
|
||||
<DialogContent className="sm:max-w-5xl">
|
||||
{prefill ? (
|
||||
<BillingDialogContent
|
||||
key={instanceKey}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ function RevealButton({ credentialId }: { credentialId: string }) {
|
|||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="min-w-32 font-mono text-small">
|
||||
<span className="font-mono text-small break-all sm:min-w-32">
|
||||
{password ?? "••••••••"}
|
||||
</span>
|
||||
<Button
|
||||
|
|
@ -86,9 +86,9 @@ export function CredentialsTable({
|
|||
<TableHeader>
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>Dienst</TableHead>
|
||||
<TableHead>Benutzername</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Benutzername</TableHead>
|
||||
<TableHead>Passwort</TableHead>
|
||||
<TableHead className="w-24" />
|
||||
<TableHead className="w-20 sm:w-24" />
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -102,10 +102,13 @@ export function CredentialsTable({
|
|||
<div className="flex size-9 shrink-0 items-center justify-center rounded-md border border-border bg-background">
|
||||
<ServiceLogo serviceId={credential.service} size={18} className="size-4.5 object-contain" />
|
||||
</div>
|
||||
<span className="font-medium">{service.label}</span>
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="font-medium">{service.label}</span>
|
||||
<span className="text-small text-muted-foreground break-all md:hidden">{credential.username}</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{credential.username}</TableCell>
|
||||
<TableCell className="hidden text-muted-foreground break-all md:table-cell">{credential.username}</TableCell>
|
||||
<TableCell>
|
||||
<RevealButton credentialId={credential.id} />
|
||||
</TableCell>
|
||||
|
|
|
|||
|
|
@ -108,16 +108,16 @@ function PreviewContent({ doc, url }: { doc: DocumentItem; url: string }) {
|
|||
switch (kind) {
|
||||
case "image":
|
||||
// eslint-disable-next-line @next/next/no-img-element -- signierte, kurzlebige Storage-URL
|
||||
return <img src={url} alt={doc.name} className="mx-auto max-h-[70vh] w-auto rounded-md object-contain" />;
|
||||
return <img src={url} alt={doc.name} className="mx-auto max-h-[70dvh] w-auto max-w-full rounded-md object-contain" />;
|
||||
case "pdf":
|
||||
return <iframe src={url} title={doc.name} className="h-[70vh] w-full rounded-md border border-border" />;
|
||||
return <iframe src={url} title={doc.name} className="h-[60dvh] w-full rounded-md border border-border sm:h-[70dvh]" />;
|
||||
case "video":
|
||||
return <video src={url} controls className="max-h-[70vh] w-full rounded-md" />;
|
||||
return <video src={url} controls className="max-h-[70dvh] w-full rounded-md" />;
|
||||
case "audio":
|
||||
return <audio src={url} controls className="w-full" />;
|
||||
case "text":
|
||||
return (
|
||||
<pre className="max-h-[70vh] overflow-auto rounded-md border border-border bg-surface p-4 text-small whitespace-pre-wrap break-words">
|
||||
<pre className="max-h-[70dvh] overflow-auto rounded-md border border-border bg-surface p-4 text-small whitespace-pre-wrap break-words">
|
||||
{text ?? "Wird geladen..."}
|
||||
</pre>
|
||||
);
|
||||
|
|
@ -185,7 +185,7 @@ function PreviewDialog({
|
|||
|
||||
return (
|
||||
<Dialog open={doc !== null} onOpenChange={handleOpenChange}>
|
||||
<DialogContent className="max-h-[95vh] overflow-y-auto sm:max-w-4xl">
|
||||
<DialogContent className="max-h-[95dvh] overflow-y-auto sm:max-w-4xl">
|
||||
{doc && (
|
||||
<>
|
||||
<DialogHeader>
|
||||
|
|
@ -398,7 +398,7 @@ export function DocumentsBrowser({
|
|||
</label>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="secondary" onClick={() => handleDownload(visibleSelection)} disabled={visibleSelection.length === 0 || busy}>
|
||||
{downloading ? <LoaderCircleIcon className="size-4 animate-spin" /> : <DownloadIcon className="size-4" />}
|
||||
{downloading ? "Wird vorbereitet..." : visibleSelection.length > 1 ? `Download (${visibleSelection.length})` : "Download"}
|
||||
|
|
@ -427,7 +427,7 @@ export function DocumentsBrowser({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => inputRef.current?.click()}
|
||||
className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-border bg-surface/50 p-12 text-center text-muted-foreground transition-colors hover:bg-surface"
|
||||
className="flex flex-col items-center gap-2 rounded-lg border border-dashed border-border bg-surface/50 p-8 text-center text-muted-foreground transition-colors hover:bg-surface sm:p-12"
|
||||
>
|
||||
<UploadIcon className="size-8" />
|
||||
<span>Noch keine Dateien. Hierher ziehen oder klicken zum Hochladen.</span>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ function NoteItem({ profileId, note }: { profileId: string; note: InternalNote }
|
|||
<NoteForm profileId={profileId} note={note} onDone={() => setEditing(false)} onCancel={() => setEditing(false)} />
|
||||
) : (
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<p className="whitespace-pre-wrap break-words">{note.body}</p>
|
||||
<p className="min-w-0 whitespace-pre-wrap break-words">{note.body}</p>
|
||||
<div className="flex shrink-0 gap-1">
|
||||
<Button variant="ghost" size="icon-sm" aria-label="Notiz bearbeiten" onClick={() => setEditing(true)}>
|
||||
<PencilIcon className="size-4" />
|
||||
|
|
@ -87,7 +87,7 @@ export function InternalNotes({ profileId, notes }: { profileId: string; notes:
|
|||
return (
|
||||
<section className="flex flex-col gap-4 rounded-lg border border-dashed border-border bg-surface/50 p-4 md:p-6">
|
||||
<div className="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 className="text-xl font-semibold">Interne Notiz</h2>
|
||||
<h2 className="text-lg font-semibold md:text-xl">Interne Notiz</h2>
|
||||
<span className="inline-flex items-center gap-1.5 rounded-full bg-ink-100 px-2.5 py-0.5 text-label text-ink-700 dark:bg-ink-800 dark:text-ink-300">
|
||||
<EyeOffIcon className="size-3.5" /> Kunde sieht das nicht
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -204,7 +204,7 @@ export function InvoicesTable({
|
|||
</InputGroupAddon>
|
||||
</InputGroup>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
<Button variant="secondary" onClick={handleDownload} disabled={selectedVisible.length === 0 || downloading}>
|
||||
<ArrowDownToLineIcon />
|
||||
{downloading
|
||||
|
|
@ -242,14 +242,14 @@ export function InvoicesTable({
|
|||
<TableHead>Rechnung</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Betrag</TableHead>
|
||||
<TableHead>Fällig am</TableHead>
|
||||
<TableHead className="hidden sm:table-cell">Fällig am</TableHead>
|
||||
{isAdmin && <TableHead className="w-12" />}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{visible.length === 0 ? (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={isAdmin ? 6 : 5} className="py-8 text-center text-muted-foreground">
|
||||
<TableCell colSpan={isAdmin ? 6 : 5} className="py-8 text-center text-muted-foreground wrap-anywhere">
|
||||
Keine Treffer für „{query}“.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
|
|
@ -275,18 +275,21 @@ export function InvoicesTable({
|
|||
href={invoiceUrl(invoice.id)}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="hover:underline underline-offset-4"
|
||||
className="whitespace-nowrap hover:underline underline-offset-4"
|
||||
>
|
||||
{invoice.invoiceNumber}
|
||||
</a>
|
||||
<span className="block text-small font-normal whitespace-nowrap text-muted-foreground tabular-nums sm:hidden">
|
||||
fällig {formatDate(invoice.dueDate)}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={STATUS_VARIANTS[invoice.displayStatus]}>
|
||||
{STATUS_LABELS[invoice.displayStatus]}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right tabular-nums">{formatCurrency(invoice.grossTotal)}</TableCell>
|
||||
<TableCell className="tabular-nums text-muted-foreground">{formatDate(invoice.dueDate)}</TableCell>
|
||||
<TableCell className="text-right tabular-nums whitespace-nowrap">{formatCurrency(invoice.grossTotal)}</TableCell>
|
||||
<TableCell className="hidden tabular-nums text-muted-foreground sm:table-cell">{formatDate(invoice.dueDate)}</TableCell>
|
||||
{isAdmin && (
|
||||
<TableCell>
|
||||
<div className="flex justify-end">
|
||||
|
|
|
|||
24
modules/shared/ui/components/mobile-header.tsx
Normal file
24
modules/shared/ui/components/mobile-header.tsx
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
import Image from "next/image";
|
||||
import { SidebarTrigger } from "@/components/ui/sidebar";
|
||||
|
||||
export function MobileHeader() {
|
||||
return (
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center gap-3 border-b border-border bg-background/95 px-3 backdrop-blur supports-backdrop-filter:bg-background/80 md:hidden">
|
||||
<SidebarTrigger className="size-9" />
|
||||
<Image
|
||||
src="/pictures/since3-logo.svg"
|
||||
alt="Since3"
|
||||
width={1617}
|
||||
height={332}
|
||||
className="h-5 w-auto dark:hidden"
|
||||
/>
|
||||
<Image
|
||||
src="/pictures/since3-logo-weiss.svg"
|
||||
alt="Since3"
|
||||
width={1617}
|
||||
height={332}
|
||||
className="hidden h-5 w-auto dark:block"
|
||||
/>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
|
@ -29,8 +29,8 @@ export function ProductCheckLine({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1 text-small">
|
||||
<span aria-live="polite" className={cn(lastCheck && !lastCheck.ok ? "text-signal-error" : "text-muted-foreground")}>
|
||||
<div className="flex items-start gap-1 text-small sm:items-center">
|
||||
<span aria-live="polite" className={cn("min-w-0 wrap-anywhere", lastCheck && !lastCheck.ok ? "text-signal-error" : "text-muted-foreground")}>
|
||||
{pending
|
||||
? "Wird geprüft..."
|
||||
: error ??
|
||||
|
|
@ -44,7 +44,7 @@ export function ProductCheckLine({
|
|||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="size-6"
|
||||
className="size-6 shrink-0"
|
||||
onClick={handleCheck}
|
||||
disabled={pending}
|
||||
aria-label="Erreichbarkeit jetzt prüfen"
|
||||
|
|
|
|||
|
|
@ -43,9 +43,9 @@ export async function BillingPanel({
|
|||
const invoices = clientId ? await getInvoices(clientId) : []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<h1 className="text-3xl font-semibold">Rechnungen</h1>
|
||||
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Rechnungen</h1>
|
||||
</div>
|
||||
<InvoicesTable
|
||||
profileId={profileId}
|
||||
|
|
|
|||
|
|
@ -14,9 +14,9 @@ export async function CredentialsPanel({
|
|||
const credentials = clientId ? await getCredentials(clientId) : []
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<h1 className="text-3xl font-semibold">Passwörter</h1>
|
||||
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Passwörter</h1>
|
||||
<CredentialDialog profileId={profileId} />
|
||||
</div>
|
||||
<CredentialsTable profileId={profileId} credentials={credentials} isAdmin={isAdmin} />
|
||||
|
|
|
|||
|
|
@ -26,8 +26,8 @@ export async function DocumentsPanel({
|
|||
|
||||
if (!clientId) {
|
||||
return (
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
<h1 className="text-3xl font-semibold">Dokumente</h1>
|
||||
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Dokumente</h1>
|
||||
<EmptyState>
|
||||
{isAdmin
|
||||
? "Für diesen Client sind noch keine Stammdaten hinterlegt. Bitte zuerst die Stammdaten ausfüllen."
|
||||
|
|
@ -46,9 +46,9 @@ export async function DocumentsPanel({
|
|||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<nav aria-label="Pfad" className="flex min-w-0 items-center gap-1.5 text-3xl font-semibold">
|
||||
<nav aria-label="Pfad" className="flex min-w-0 items-center gap-1.5 text-2xl font-semibold md:text-3xl">
|
||||
<Link href={basePath} className="text-muted-foreground hover:text-foreground">Dokumente</Link>
|
||||
<ChevronRightIcon className="size-6 shrink-0 text-muted-foreground" aria-hidden />
|
||||
<ChevronRightIcon className="size-5 shrink-0 text-muted-foreground md:size-6" aria-hidden />
|
||||
<span className="truncate">{folder.name}</span>
|
||||
</nav>
|
||||
{isAdmin && documents.length === 0 && (
|
||||
|
|
@ -65,7 +65,7 @@ export async function DocumentsPanel({
|
|||
return (
|
||||
<div className="flex flex-col gap-6 w-full">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-3xl font-semibold">Dokumente</h1>
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Dokumente</h1>
|
||||
{isAdmin && <CreateFolderDialog profileId={profileId} />}
|
||||
</div>
|
||||
|
||||
|
|
@ -79,9 +79,9 @@ export async function DocumentsPanel({
|
|||
<li key={folder.id}>
|
||||
<Link
|
||||
href={`${basePath}?folder=${folder.id}`}
|
||||
className="flex flex-col items-center gap-2 rounded-lg border border-border bg-background p-5 text-center transition-colors hover:border-ink-300 hover:bg-surface focus-visible:ring-2 focus-visible:ring-ring outline-none"
|
||||
className="flex flex-col items-center gap-2 rounded-lg border border-border bg-background p-4 text-center sm:p-5 transition-colors hover:border-ink-300 hover:bg-surface focus-visible:ring-2 focus-visible:ring-ring outline-none"
|
||||
>
|
||||
<FolderIcon className="size-12 fill-accent-400/20 stroke-[1.25] text-accent-500" aria-hidden />
|
||||
<FolderIcon className="size-10 sm:size-12 fill-accent-400/20 stroke-[1.25] text-accent-500" aria-hidden />
|
||||
<span className="w-full truncate font-medium">{folder.name}</span>
|
||||
<span className="text-label text-muted-foreground">
|
||||
{folder.documentCount} {folder.documentCount === 1 ? "Datei" : "Dateien"}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function SectionHeader({ title, meta, action }: { title: React.ReactNode; meta?:
|
|||
return (
|
||||
<div className="flex flex-wrap items-end justify-between gap-2">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<h2 className="text-xl font-semibold">{title}</h2>
|
||||
<h2 className="text-lg font-semibold md:text-xl">{title}</h2>
|
||||
{meta && <p className="text-small text-muted-foreground">{meta}</p>}
|
||||
</div>
|
||||
{action}
|
||||
|
|
@ -115,9 +115,9 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
const today = berlinToday()
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10 w-full">
|
||||
<div className="flex w-full flex-col gap-8 md:gap-10">
|
||||
<div className="w-full flex flex-col justify-between gap-2">
|
||||
<h1 className="text-3xl font-semibold">Info</h1>
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Info</h1>
|
||||
<p className="text-accent-400">Status eurer Systeme und laufende Arbeiten auf einen Blick.</p>
|
||||
</div>
|
||||
|
||||
|
|
@ -147,7 +147,7 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
<EmptyState>Noch keine Systeme hinterlegt.</EmptyState>
|
||||
) : (
|
||||
<>
|
||||
<div role="status" className={cn("flex items-center gap-3 rounded-lg border px-4 py-3 font-medium", banner.className)}>
|
||||
<div role="status" className={cn("flex items-start gap-3 rounded-lg border px-4 py-3 font-medium sm:items-center", banner.className)}>
|
||||
<banner.icon className="size-5 shrink-0" />
|
||||
{banner.text}
|
||||
</div>
|
||||
|
|
@ -158,8 +158,8 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
<TableRow className="hover:bg-transparent">
|
||||
<TableHead>Produkt</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Uptime</TableHead>
|
||||
{isAdmin && <TableHead className="w-24" />}
|
||||
<TableHead className="hidden text-right sm:table-cell">Uptime</TableHead>
|
||||
{isAdmin && <TableHead className="w-20 sm:w-24" />}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
|
|
@ -173,11 +173,14 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
aria-label={PRODUCT_STATUS_LABELS[product.status]}
|
||||
title={PRODUCT_STATUS_LABELS[product.status]}
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<div className="flex min-w-0 flex-col">
|
||||
<span className="font-medium">{product.name}</span>
|
||||
<span className="text-small text-muted-foreground">
|
||||
<span className="text-small text-muted-foreground wrap-anywhere">
|
||||
{product.url ? <ProductLink value={product.url} /> : CATEGORY_LABELS[product.category]}
|
||||
</span>
|
||||
<span className="text-small tabular-nums sm:hidden">
|
||||
<UptimeValue value={displayedUptime(product)} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
|
|
@ -196,7 +199,7 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
</div>
|
||||
</TableCell>
|
||||
<TableCell
|
||||
className="text-right tabular-nums whitespace-nowrap"
|
||||
className="hidden text-right tabular-nums whitespace-nowrap sm:table-cell"
|
||||
title={product.checks30d > 0 ? `Gemessen aus ${product.checks30d} Prüfungen` : undefined}
|
||||
>
|
||||
<UptimeValue value={displayedUptime(product)} />
|
||||
|
|
@ -234,19 +237,21 @@ const InfoPanel = async ({ profileId, isAdmin }: { profileId: string; isAdmin: b
|
|||
) : (
|
||||
<ul className="flex flex-col divide-y divide-border rounded-lg border border-border">
|
||||
{tasks.map((task) => (
|
||||
<li key={task.id} className="flex items-center gap-4 px-4 py-3">
|
||||
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="w-20">
|
||||
<li key={task.id} className="flex items-start gap-3 px-4 py-3 sm:items-center sm:gap-4">
|
||||
<Badge variant={task.status === "in_progress" ? "warning" : "neutral"} className="mt-0.5 w-20 shrink-0 sm:mt-0">
|
||||
{TASK_STATUS_LABELS[task.status]}
|
||||
</Badge>
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="font-medium">{task.name}</span>
|
||||
{task.description && (
|
||||
<span className="text-small text-muted-foreground">{task.description}</span>
|
||||
)}
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-0.5 sm:flex-row sm:items-center sm:gap-4">
|
||||
<div className="flex min-w-0 flex-1 flex-col">
|
||||
<span className="font-medium break-words">{task.name}</span>
|
||||
{task.description && (
|
||||
<span className="text-small text-muted-foreground break-words">{task.description}</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-small text-muted-foreground tabular-nums sm:shrink-0">
|
||||
{formatTaskTiming(task.status, task.since, today)}
|
||||
</span>
|
||||
</div>
|
||||
<span className="shrink-0 text-small text-muted-foreground tabular-nums">
|
||||
{formatTaskTiming(task.status, task.since, today)}
|
||||
</span>
|
||||
{isAdmin && (
|
||||
<div className="flex shrink-0 gap-1">
|
||||
<TaskDialog profileId={profileId} task={task} />
|
||||
|
|
|
|||
|
|
@ -26,9 +26,9 @@ export function StammdatenPanel({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8 w-full">
|
||||
<div className="w-full flex flex-row justify-between items-center">
|
||||
<h1 className="text-3xl font-semibold">Stammdaten</h1>
|
||||
<div className="flex w-full flex-col gap-6 md:gap-8">
|
||||
<div className="flex w-full flex-wrap items-center justify-between gap-3">
|
||||
<h1 className="text-2xl font-semibold md:text-3xl">Stammdaten</h1>
|
||||
{!isEditing && (
|
||||
<Button variant="outline" onClick={() => setIsEditing(true)}>
|
||||
<Pen className="w-4 h-4 mr-2" /> Edit
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue