Files
HealthLog/src/components/documents/upload-zone.tsx
T
Marc-André Bombeck cd450a8186 fix(documents): conform the vault page to the app frame and trim its chrome
- width: drop the /documents wide-content exception; the page uses the
  standard content frame like every other route (the multi-column
  timeline still fits, and no page should be wider than the rest).
- upload: retire the standing dashed drop card — it took a vertical band
  on every visit for what the header Upload button and the page-wide
  drag-and-drop overlay already cover. The hidden input and the
  above-80% quota bar stay.
- filter: search shares one row with the tag chips on desktop instead of
  sitting on its own line above them.
- detail: move Delete to the bottom-left as a quiet text button; the
  bottom-right slot reads as the primary/confirm action, so a
  destructive control there is a footgun. Download keeps the trailing edge.
2026-07-07 15:56:30 +02:00

90 lines
3.1 KiB
TypeScript

"use client";
/**
* The vault's upload plumbing: the hidden file input the page header's
* Upload button opens, plus a quota bar that appears only above 80 %
* usage. There is no standing drop-target card — it took a full band of
* vertical space on every visit for an affordance the header button and
* the page-wide drag-and-drop overlay already cover. Dropping a file
* anywhere on the page is caught by that overlay; the header button opens
* this input.
*
* The `accept` list comes verbatim from the usage endpoint (HEIC is
* deliberately absent so the iOS picker transcodes camera photos to JPEG),
* `multiple` is on, and there is NO `capture` attribute — mobile Safari
* then offers camera AND library.
*/
import { type RefObject } from "react";
import { Progress } from "@/components/ui/progress";
import { useTranslations } from "@/lib/i18n/context";
import type { DocumentUsageDto } from "@/lib/validations/inbound-documents";
import { formatBytes } from "./vault-utils";
/** Usage fraction above which the quota bar appears. */
const QUOTA_BAR_THRESHOLD = 0.8;
export function UploadZone({
usage,
onFiles,
inputRef,
}: {
/** Limits + usage from `GET /api/documents/inbound/usage` (undefined while loading). */
usage: DocumentUsageDto | undefined;
onFiles: (files: File[]) => void;
/** Exposed so the page header's Upload button can open the same picker. */
inputRef: RefObject<HTMLInputElement | null>;
}) {
const { t, locale } = useTranslations();
const accept = usage?.acceptedExtensions.join(",");
const usedFraction =
usage && usage.quotaBytes > 0 ? usage.usedBytes / usage.quotaBytes : 0;
const pickFiles = (list: FileList | null) => {
if (!list || list.length === 0) return;
onFiles(Array.from(list));
};
return (
<div data-slot="document-upload-zone">
<input
ref={inputRef}
type="file"
multiple
accept={accept}
className="sr-only"
aria-label={t("documents.upload.browse")}
onChange={(e) => {
pickFiles(e.target.files);
// Allow re-selecting the same file (duplicate flow) immediately.
e.target.value = "";
}}
/>
{usage && usedFraction >= QUOTA_BAR_THRESHOLD ? (
<div data-slot="document-quota-bar" className="space-y-1">
<div className="flex items-center justify-between gap-3">
<p className="text-sm font-medium">
{t("documents.upload.quotaAlmostFull")}
</p>
<p className="text-muted-foreground text-xs tabular-nums">
{t("documents.upload.quotaUsed", {
used: formatBytes(usage.usedBytes, locale),
quota: formatBytes(usage.quotaBytes, locale),
})}
</p>
</div>
<Progress
value={Math.min(100, usedFraction * 100)}
aria-label={t("documents.upload.quotaUsed", {
used: formatBytes(usage.usedBytes, locale),
quota: formatBytes(usage.quotaBytes, locale),
})}
/>
</div>
) : null}
</div>
);
}