Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ImageUpload component #563

Draft
wants to merge 27 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
4857ace
feat(ImageCropper): add ImageCropper
Lisa18289 Jul 2, 2024
e8ce4b3
feat(FileDropZone): add FileDropZone
Lisa18289 Jul 2, 2024
66a3734
feat(FileDropZone): add FileDropZone
Lisa18289 Jul 2, 2024
acf3a5a
feat(ImageUpload): add canvas controller
Lisa18289 Jul 2, 2024
93b9a67
feat(ImageUpload): add canvas controller
Lisa18289 Jul 2, 2024
c088d7b
feat(ImageUpload): stories
Lisa18289 Jul 2, 2024
67bdcc8
docs: add upload components
Lisa18289 Jul 2, 2024
006382c
docs: add upload components
Lisa18289 Jul 2, 2024
1c1383c
docs(FileDropZone): add examples for FileDropZone
Lisa18289 Jul 2, 2024
47f0399
docs(FileDropZone): add examples for FileDropZone
Lisa18289 Jul 2, 2024
b1149f3
docs(FileTrigger): add examples for FileTrigger
Lisa18289 Jul 3, 2024
884f503
docs(ImageCropper): add examples
Lisa18289 Jul 3, 2024
717fd69
Merge remote-tracking branch 'refs/remotes/origin/main' into 303-imag…
Lisa18289 Jul 3, 2024
957f958
docs: update label headings
Lisa18289 Jul 3, 2024
82748d0
Merge remote-tracking branch 'refs/remotes/origin/main' into 303-imag…
Lisa18289 Jul 4, 2024
e547625
feat(FileDropZone): throw error if file invalid
Lisa18289 Jul 4, 2024
6498aa1
fix(ImageUpload): fix fileReader for nextjs
Lisa18289 Jul 5, 2024
a9eb173
feat(ImageUpload): add remove image button
Lisa18289 Jul 5, 2024
de5fe6c
refactor(ImageUpload): update controller
Lisa18289 Jul 5, 2024
23c7814
refactor(ImageUpload): update controller
Lisa18289 Jul 5, 2024
7112c4d
Merge remote-tracking branch 'refs/remotes/origin/main' into 303-imag…
Lisa18289 Jul 22, 2024
0158667
docs(ImageCropper): Update name
Lisa18289 Jul 22, 2024
701cdc4
Merge remote-tracking branch 'refs/remotes/origin/main' into 303-imag…
Lisa18289 Jul 31, 2024
12d34f1
Merge remote-tracking branch 'origin/main' into 303-imagecropper-comp…
mfal Oct 9, 2024
f14d386
fix(FileDropZone): fix token
Lisa18289 Oct 9, 2024
325834a
refactor(FileDropZone): use IllustratedMessage
Lisa18289 Oct 10, 2024
06972db
refactor(FileDropZone): use IllustratedMessage
Lisa18289 Oct 10, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .pnp.cjs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions packages/components/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@
"types": "./dist/types/components/FieldError/index.d.ts",
"import": "./dist/FieldError.js"
},
"./FileDropZone": {
"types": "./dist/types/components/FileDropZone/index.d.ts",
"import": "./dist/FileDropZone.js"
},
"./FileTrigger": {
"types": "./dist/types/components/FileTrigger/index.d.ts",
"import": "./dist/FileTrigger.js"
},
"./Header": {
"types": "./dist/types/components/Header/index.d.ts",
"import": "./dist/Header.js"
Expand Down Expand Up @@ -134,6 +142,14 @@
"types": "./dist/types/components/Image/index.d.ts",
"import": "./dist/Image.js"
},
"./ImageCropper": {
"types": "./dist/types/components/ImageCropper/index.d.ts",
"import": "./dist/ImageCropper.js"
},
"./ImageUpload": {
"types": "./dist/types/components/ImageUpload/index.d.ts",
"import": "./dist/ImageUpload.js"
},
"./Initials": {
"types": "./dist/types/components/Initials/index.d.ts",
"import": "./dist/Initials.js"
Expand Down Expand Up @@ -333,6 +349,7 @@
"react-aria": "^3.35.0",
"react-aria-components": "^1.4.0",
"react-children-utilities": "^2.10.0",
"react-easy-crop": "^5.0.7",
"react-stately": "^3.33.0",
"remeda": "^2.15.0",
"use-callback-ref": "^1.3.2",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
.fileDropZone {
border-style: var(--file-drop-zone--border-style);
border-width: var(--file-drop-zone--border-width);
border-color: var(--file-drop-zone--border-color);
border-radius: var(--file-drop-zone--corner-radius);
padding: var(--file-drop-zone--padding);
row-gap: var(--file-drop-zone--content-to-content-spacing);

&[data-focus-visible],
&[data-drop-target] {
border-color: var(--file-drop-zone--target-border-color);
border-style: var(--file-drop-zone--target-border-style);
}

&[data-drop-target] {
background: var(--file-drop-zone--target-background-color);
}
}
73 changes: 73 additions & 0 deletions packages/components/src/components/FileDropZone/FileDropZone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import type { FC, PropsWithChildren } from "react";
import React from "react";
import type { FileTriggerProps } from "react-aria-components";
import * as Aria from "react-aria-components";
import { Button } from "@/components/Button";
import clsx from "clsx";
import styles from "./FileDropZone.module.scss";
import type { PropsWithClassName } from "@/lib/types/props";
import locales from "./locales/*.locale.json";
import { useLocalizedStringFormatter } from "react-aria";
import type FileController from "@/components/FileTrigger/FileController";
import { FileTrigger } from "@/components/FileTrigger";
import { IllustratedMessage } from "@/components/IllustratedMessage";
import { IconPicture, IconUpload } from "@/components/Icon/components/icons";
import { Heading } from "@/components/Heading";

export interface FileDropZoneProps
extends PropsWithClassName,
PropsWithChildren,
Pick<FileTriggerProps, "allowsMultiple" | "acceptedFileTypes"> {
controller: FileController;
/** @default "file" */
type?: "file" | "image";
}

export const FileDropZone: FC<FileDropZoneProps> = (props) => {
const {
controller,
allowsMultiple,
acceptedFileTypes,
className,
children,
type = "file",
} = props;

const rootClassName = clsx(styles.fileDropZone, className);

const stringFormatter = useLocalizedStringFormatter(locales);

return (
<>
<Aria.DropZone
className={rootClassName}
onDrop={(e) =>
controller.dropFile(e, acceptedFileTypes, allowsMultiple)
}
>
<IllustratedMessage color="dark">
{type === "file" ? <IconUpload /> : <IconPicture />}
<Heading>
{stringFormatter.format(
`fileDropZone.${type}.drop${allowsMultiple ? ".multiple" : ""}`,
)}
</Heading>
{children}
<FileTrigger
acceptedFileTypes={acceptedFileTypes}
allowsMultiple={allowsMultiple}
controller={controller}
>
<Button variant="outline">
{stringFormatter.format(
`fileDropZone.${type}.select${allowsMultiple ? ".multiple" : ""}`,
)}
</Button>
</FileTrigger>
</IllustratedMessage>
</Aria.DropZone>
</>
);
};

export default FileDropZone;
4 changes: 4 additions & 0 deletions packages/components/src/components/FileDropZone/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { FileDropZone } from "./FileDropZone";

export { type FileDropZoneProps, FileDropZone } from "./FileDropZone";
export default FileDropZone;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"fileDropZone.file.drop": "Datei ablegen",
"fileDropZone.file.drop.multiple": "Dateien ablegen",
"fileDropZone.file.select": "Datei auswählen",
"fileDropZone.file.select.multiple": "Dateien auswählen",
"fileDropZone.image.drop": "Bild ablegen",
"fileDropZone.image.drop.multiple": "Bilder ablegen",
"fileDropZone.image.select": "Bild auswählen",
"fileDropZone.image.select.multiple": "Bilder auswählen"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"fileDropZone.file.drop": "Drop file",
"fileDropZone.file.drop.multiple": "Drop files",
"fileDropZone.file.select": "Select file",
"fileDropZone.file.select.multiple": "Select files",
"fileDropZone.image.drop": "Drop image",
"fileDropZone.image.drop.multiple": "Drop images",
"fileDropZone.image.select": "Select image",
"fileDropZone.image.select.multiple": "Select images"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { FileDropZone } from "@/components/FileDropZone";
import { Text } from "@/components/Text";
import { LabeledValue } from "@/components/LabeledValue";
import { Label } from "@/components/Label";
import { Section } from "@/components/Section";
import { useFileController } from "@/components/FileTrigger";

const meta: Meta<typeof FileDropZone> = {
title: "Upload/FileDropZone",
component: FileDropZone,
parameters: {
controls: { exclude: ["className", "controller"] },
},
render: (props) => {
const controller = useFileController();
const files = controller.useFiles();

return (
<Section>
<FileDropZone {...props} controller={controller} />
<LabeledValue>
<Label>Selected files</Label>
<Text>
{files.length > 0 ? files.map((f) => f.name).join(", ") : "-"}
</Text>
</LabeledValue>
</Section>
);
},
};
export default meta;

type Story = StoryObj<typeof FileDropZone>;

export const Default: Story = {};

export const AllowsMultiple: Story = { args: { allowsMultiple: true } };

export const AcceptedFileTypes: Story = {
args: { acceptedFileTypes: ["image/png", "image/jpeg"] },
render: (props) => {
const controller = useFileController();
const files = controller.useFiles();

return (
<Section>
<FileDropZone {...props} controller={controller}>
<Text>Accepted file types are jpg and png.</Text>
</FileDropZone>
<LabeledValue>
<Label>Selected files</Label>
<Text>
{files.length > 0 ? files.map((f) => f.name).join(", ") : "-"}
</Text>
</LabeledValue>
</Section>
);
},
};
78 changes: 78 additions & 0 deletions packages/components/src/components/FileTrigger/FileController.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { action, makeObservable, observable } from "mobx";
import useSelector from "@/lib/mobx/useSelector";
import { useRef } from "react";
import type * as Aria from "react-aria-components";
import type { DropEvent } from "@react-types/shared";

export class FileController {
public readonly files = new Map<number, File>();
private id = 0;

public constructor() {
makeObservable(this, {
files: observable.shallow,
add: action.bound,
clear: action.bound,
dropFile: action.bound,
selectFile: action.bound,
});
}

public static useNew(): FileController {
return useRef(new FileController()).current;
}

public useFiles(): File[] {
return useSelector(() => Array.from(this.files.values()));
}

public add(file: File): void {
const id = this.id++;

this.files.set(id, file);
}

public clear(): void {
this.files.clear();
}

public async dropFile(
e: DropEvent,
acceptedFileTypes?: string[],
allowsMultiple?: boolean,
) {
const fileDropItems = e.items.filter(
(file) => file.kind === "file",
) as Aria.FileDropItem[];

fileDropItems.map(async (f, i) => {
const file = await f.getFile();

if (acceptedFileTypes && !acceptedFileTypes.includes(file.type)) {
throw new Error("invalid file type");
}

if (!allowsMultiple && i > 0) {
throw new Error("invalid file count");
}

if (!allowsMultiple) {
this.clear();
}

this.add(file);
});
}

public selectFile(e: FileList | null, allowsMultiple?: boolean) {
const files = e ? Array.from(e) : [];
files.map((f) => {
if (!allowsMultiple) {
this.clear();
}
this.add(f);
});
}
}

export default FileController;
Loading