# file

## Client

`import { ... } from "wcz-layout/data/client";`

- Query:
  `fileMetasQueryOptions(GetFileMetasSchema)` → `FileMeta[]` // metadata of all files under `subId`
  `fileThumbnailQueryOptions(FileSchema)` → `string` // thumbnail as object URL, disabled while `id` is empty
  `fileQueryOptions(FileSchema)` → `string` // file content as object URL for preview, disabled while `id` is empty
- Mutation:
  `downloadFileMutationOptions()` → `mutate(FileSchema & { fileName, fileExtension })` // downloads and saves as `fileName.fileExtension`
  `openFileMutationOptions({ target })` → `mutate(FileSchema)` // opens file in new tab; `target` is a `useRef<Window | null>`, open `about:blank` into it synchronously in the click handler to avoid popup blockers
  `updateFileMetaMutationOptions({ queryClient })` → `mutate(UpdateFileMetaSchema)` // update metadata (e.g. rename `fileName`), invalidates metas
  `deleteFileMutationOptions({ queryClient })` → `mutate(FileSchema)` // delete single file, invalidates metas
  `batchDeleteFilesMutationOptions({ queryClient })` → `mutate(GetFileMetasSchema)` // delete all files under `subId`, invalidates metas
  `uploadFileMutationOptions({ queryClient })` → `mutate(UploadFileMetaSchema & { setProgress? })` → `{ id, appName, subId, fileName }` // uploads directly from browser via TUS (1 MB chunks), `setProgress(percent)` for a progress bar, invalidates metas of `subId`

## Server

`import { ... } from "wcz-layout/data/server";`

- Fetch:
  `getFileMetas({ data: GetFileMetasSchema })` → `FileMeta[]` // metadata of all files under `subId`
  `getFileThumbnail({ data: FileSchema })` → `Response` // thumbnail binary stream
  `getFile({ data: FileSchema })` → `Response` // file binary stream, `Content-Disposition: inline`
  `downloadFile({ data: FileSchema })` → `Response` // file binary stream for download
- Mutate:
  `updateFileMeta({ data: UpdateFileMetaSchema })` → `void` // update metadata
  `deleteFile({ data: FileSchema })` → `void` // delete single file
  `batchDeleteFiles({ data: GetFileMetasSchema })` → `void` // delete all files under `subId`
  `uploadFile({ data: UploadFileMetaSchema })` → `{ id, appName, subId, fileName }` // server-originated uploads only, client uploads go direct via `uploadFileMutationOptions`

## Components

`import { Dropzone, FileViewer } from "wcz-layout/components";`

- `<Dropzone onDrop progress? sx? {...DropzoneOptions} />` // react-dropzone area (`accept`, `maxFiles`, `disabled`, `onDropAccepted`, …), `progress` (0-100) shows upload progress
- `<FileViewer appName subId actions? onDelete?>{(component) => <component.Grid /> | <component.List />}</FileViewer>` // fetches metas itself, renders nothing when there are no files
  - `actions?: { download?: boolean, delete?: boolean }` // both enabled by default
  - `onDelete?: ({ remainingFileMetas: FileMeta[], deletedFileMeta: FileMeta }) => void`
  - `component.Grid` props: `size?: number` (default 150), `itemBar?: "hidden" | "always" | "onMouseEnter"` (default on hover), `sx?`
  - `component.List` props: `sx?`

## Schemas/Models

```ts
const FileMetaSchema = z.object({
  id: z.uuid(),
  subId: z.uuid(),
  appName: z.string().trim().min(1).max(255),
  fileName: z.string().trim().min(1).max(255),
  fileExtension: z.string().trim().min(1).max(255),
  fileSize: z.number().min(0),
  mediaSubType: z.string().trim().min(1).max(255),
  mediaType: z.string().trim().min(1).max(255),
  mimeType: z.string().trim().min(1).max(255),
  createdBy: z.string().trim().min(1).max(255),
  createdDate: z.date(),
});

const GetFileMetasSchema = z.object({
  appName: z.string().min(1).max(255),
  subId: z.uuid(),
});

const FileSchema = z.object({
  appName: z.string().min(1).max(255),
  id: z.uuid(),
});

const UpdateFileMetaSchema = FileMetaSchema.extend({
  fileName: z.string().min(1).max(255),
});

const UploadFileMetaSchema = z.object({
  id: z.uuid().default(uuidv7),
  appName: z.string().min(1).max(255),
  subId: z.uuid(),
  file: z.instanceof(File),
});
```

## Examples

```tsx
import { useRef, useState } from "react";
import { useMutation, useQuery } from "@tanstack/react-query";
import {
  deleteFileMutationOptions,
  downloadFileMutationOptions,
  fileMetasQueryOptions,
  fileQueryOptions,
  openFileMutationOptions,
  uploadFileMutationOptions,
} from "wcz-layout/data/client";
import { Dropzone, FileViewer } from "wcz-layout/components";
import { useInView } from "wcz-layout/hooks";

const { queryClient } = Route.useRouteContext();

// fetch metas (lazily when in view) + fetch first file
const { ref, inView } = useInView({ triggerOnce: true });

const { data: fileMetas = [] } = useQuery({
  ...fileMetasQueryOptions({ appName: packageJson.name, subId }),
  enabled: inView,
});

const { data: file } = useQuery({
  ...fileQueryOptions({ appName: packageJson.name, id: fileMetas[0]?.id ?? "" }),
  enabled: !!fileMetas[0],
});

// upload via Dropzone
const [progress, setProgress] = useState(0);
const { mutateAsync: uploadFile } = useMutation(uploadFileMutationOptions({ queryClient }));

const onDrop = async (acceptedFiles: File[]) => {
  await Promise.allSettled(
    acceptedFiles.map((file) =>
      uploadFile({ appName: packageJson.name, subId, file, setProgress }),
    ),
  );
  setProgress(0);
};

<Dropzone onDrop={onDrop} progress={progress} />;

// download / delete
const { mutate: download } = useMutation(downloadFileMutationOptions());
const { mutate: remove } = useMutation(deleteFileMutationOptions({ queryClient }));

download({
  appName: packageJson.name,
  id: meta.id,
  fileName: meta.fileName,
  fileExtension: meta.fileExtension,
});
remove({ appName: packageJson.name, id: meta.id });

// open file
const target = useRef<Window | null>(null);
const { mutate: openFile } = useMutation(openFileMutationOptions({ target }));

// File Viewer - no manual fetch of metas needed
<FileViewer appName={packageJson.name} subId={subId}>
  {(component) => <component.Grid />} // or component.List
</FileViewer>;
```
