mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-12 06:06:10 +00:00
The document vault and the admin area each carried a formatBytes; the same backup read 1.25 MB in one and 1.2 MB in the other, and only the vault knew gigabytes. The vault rule (one decimal below 100, up to GB, digits in the reader's locale) now lives in src/lib/format/bytes.ts and takes the locale, so the server-rendered share page uses it too. A test holds the definition to that one module.
90 lines
3.1 KiB
TypeScript
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 "@/lib/format/bytes";
|
|
|
|
/** 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>
|
|
);
|
|
}
|