Skip to content

Commit 1522850

Browse files
committed
Add initial avatar and image upload fields
1 parent 172cb12 commit 1522850

4 files changed

Lines changed: 232 additions & 0 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# AvatarImageField
2+
3+
The `AvatarImageField` component is used to upload an image and display it as an avatar.
4+
5+
```tsx
6+
import { AvatarImageField } from '@/ui/components/AvatarImageField';
7+
8+
export default function Example() {
9+
return (
10+
<AvatarImageField
11+
name="image"
12+
label="Photo"
13+
placeholder="Upload an image"
14+
avatarUrl={"/windcraft-logo.webp"}
15+
initials={"W"}
16+
acceptedFileTypes={["image/png"]}
17+
/>
18+
);
19+
}
20+
```
21+
22+
## Invalid
23+
24+
```tsx
25+
import { AvatarImageField } from '@/ui/components/AvatarImageField';
26+
27+
export default function Example() {
28+
return (
29+
<AvatarImageField
30+
name="image"
31+
label="Photo"
32+
placeholder="Upload an image"
33+
avatarUrl={undefined}
34+
initials={"W"}
35+
acceptedFileTypes={["image/png"]}
36+
isInvalid
37+
/>
38+
);
39+
}
40+
```
41+
42+
## Disabled
43+
44+
```tsx
45+
import { AvatarImageField } from '@/ui/components/AvatarImageField';
46+
47+
export default function Example() {
48+
return (
49+
<AvatarImageField
50+
name="image"
51+
label="Photo"
52+
placeholder="Upload an image"
53+
avatarUrl={undefined}
54+
initials={"W"}
55+
acceptedFileTypes={["image/png"]}
56+
isDisabled
57+
/>
58+
);
59+
}
60+
```
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
# ImageField
2+
3+
The `ImageField` component is used to upload an image.
4+
5+
```tsx
6+
import { ImageField } from '@/ui/components/ImageField';
7+
8+
export default function Example() {
9+
return (
10+
<ImageField
11+
name="image"
12+
label="Image"
13+
placeholder="Upload a logo (46x46px png)"
14+
imageUrl={"/windcraft-logo.webp"}
15+
height={46}
16+
width={46}
17+
acceptedFileTypes={["image/png"]}
18+
/>
19+
);
20+
}
21+
```
22+
23+
## Invalid
24+
25+
```tsx
26+
import { ImageField } from '@/ui/components/ImageField';
27+
28+
export default function Example() {
29+
return (
30+
<ImageField
31+
name="image"
32+
label="Image"
33+
placeholder="Upload a logo (46x46px png)"
34+
imageUrl={undefined}
35+
height={46}
36+
width={46}
37+
acceptedFileTypes={["image/png"]}
38+
isInvalid
39+
/>
40+
);
41+
}
42+
```
43+
44+
## Disabled
45+
46+
```tsx
47+
import { ImageField } from '@/ui/components/ImageField';
48+
49+
export default function Example() {
50+
return (
51+
<ImageField
52+
name="image"
53+
label="Image"
54+
placeholder="Upload a logo (46x46px png)"
55+
imageUrl={undefined}
56+
height={46}
57+
width={46}
58+
acceptedFileTypes={["image/png"]}
59+
isDisabled
60+
/>
61+
);
62+
}
63+
```

ui/components/AvatarImageField.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { FileTrigger, type FileTriggerProps, InputContext } from "react-aria-components";
2+
import { Avatar } from "./Avatar";
3+
import { Button } from "./Button";
4+
import { useMemo, useState } from "react";
5+
6+
type AvatarImageFieldProps = {
7+
name: string;
8+
label?: string;
9+
placeholder: string;
10+
avatarUrl?: string;
11+
initials: string;
12+
isRound?: boolean;
13+
} & Omit<FileTriggerProps, "onSelect">;
14+
15+
export function AvatarImageField({
16+
name,
17+
label,
18+
placeholder = "Upload an image",
19+
avatarUrl,
20+
initials,
21+
isRound = true,
22+
acceptedFileTypes = ["image/png"],
23+
...props
24+
}: Readonly<AvatarImageFieldProps>) {
25+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
26+
const [filename, setFilename] = useState<string | null>(null);
27+
const inputContext = useMemo(() => ({ name: filename ? name : undefined }), [filename, name]);
28+
29+
return (
30+
<div className="flex flex-col gap-3">
31+
{label && <label>{label}</label>}
32+
<InputContext.Provider value={inputContext}>
33+
<div className="flex flex-row gap-4 items-center">
34+
<FileTrigger
35+
{...props}
36+
acceptedFileTypes={acceptedFileTypes}
37+
onSelect={(e) => {
38+
if (e) {
39+
const files = Array.from(e);
40+
const file = files[0];
41+
setFilename(file.name);
42+
setPreviewUrl(URL.createObjectURL(file));
43+
}
44+
}}
45+
>
46+
<Button variant="icon" className={isRound ? "rounded-full" : "rounded-md"}>
47+
<Avatar avatarUrl={previewUrl ?? avatarUrl} initials={initials} isRound={isRound} size="md" />
48+
</Button>
49+
</FileTrigger>
50+
{filename ?? placeholder ?? ""}
51+
</div>
52+
</InputContext.Provider>
53+
</div>
54+
);
55+
}

ui/components/ImageField.tsx

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { FileTrigger, type FileTriggerProps, InputContext } from "react-aria-components";
2+
import { Button } from "./Button";
3+
import { useMemo, useState } from "react";
4+
5+
type ImageFieldProps = {
6+
name: string;
7+
label?: string;
8+
placeholder: string;
9+
imageUrl: string;
10+
height?: number;
11+
width?: number;
12+
} & Omit<FileTriggerProps, "onSelect">;
13+
14+
export function ImageField({
15+
name,
16+
label,
17+
placeholder = "Upload an image",
18+
imageUrl,
19+
acceptedFileTypes = ["image/png"],
20+
height,
21+
width,
22+
...props
23+
}: Readonly<ImageFieldProps>) {
24+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
25+
const [filename, setFilename] = useState<string | null>(null);
26+
const inputContext = useMemo(() => ({ name: filename ? name : undefined }), [filename, name]);
27+
28+
return (
29+
<div className="flex flex-col gap-3">
30+
{label && <label>{label}</label>}
31+
<InputContext.Provider value={inputContext}>
32+
<div className="flex flex-row gap-4 items-center">
33+
<FileTrigger
34+
{...props}
35+
acceptedFileTypes={acceptedFileTypes}
36+
onSelect={(e) => {
37+
if (e) {
38+
const files = Array.from(e);
39+
const file = files[0];
40+
setFilename(file.name);
41+
setPreviewUrl(URL.createObjectURL(file));
42+
}
43+
}}
44+
>
45+
<Button variant="icon" className="rounded-md w-fit h-fit p-1">
46+
<img src={previewUrl ?? imageUrl} alt={name} height={height} width={width} />
47+
</Button>
48+
</FileTrigger>
49+
{filename ?? placeholder ?? ""}
50+
</div>
51+
</InputContext.Provider>
52+
</div>
53+
);
54+
}

0 commit comments

Comments
 (0)