Skip to content

Commit 585cf7f

Browse files
authored
Merge pull request #56 from iib0011/refactor-file-input
refactor: file inputs
2 parents 31dca53 + b762cc8 commit 585cf7f

File tree

12 files changed

+465
-26
lines changed

12 files changed

+465
-26
lines changed

.idea/workspace.xml

Lines changed: 13 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import React, { ReactNode, useContext, useEffect } from 'react';
2+
import { Box, useTheme } from '@mui/material';
3+
import Typography from '@mui/material/Typography';
4+
import InputHeader from '../InputHeader';
5+
import InputFooter from './InputFooter';
6+
import {
7+
BaseFileInputProps,
8+
createObjectURL,
9+
revokeObjectURL
10+
} from './file-input-utils';
11+
import { globalInputHeight } from '../../config/uiConfig';
12+
import { CustomSnackBarContext } from '../../contexts/CustomSnackBarContext';
13+
import greyPattern from '@assets/grey-pattern.png';
14+
15+
interface BaseFileInputComponentProps extends BaseFileInputProps {
16+
children: (props: { preview: string | undefined }) => ReactNode;
17+
type: 'image' | 'video' | 'audio';
18+
}
19+
20+
export default function BaseFileInput({
21+
value,
22+
onChange,
23+
accept,
24+
title,
25+
children,
26+
type
27+
}: BaseFileInputComponentProps) {
28+
const [preview, setPreview] = React.useState<string | null>(null);
29+
const theme = useTheme();
30+
const fileInputRef = React.useRef<HTMLInputElement>(null);
31+
const { showSnackBar } = useContext(CustomSnackBarContext);
32+
33+
useEffect(() => {
34+
if (value) {
35+
const objectUrl = createObjectURL(value);
36+
setPreview(objectUrl);
37+
38+
return () => revokeObjectURL(objectUrl);
39+
} else {
40+
setPreview(null);
41+
}
42+
}, [value]);
43+
44+
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
45+
const file = event.target.files?.[0];
46+
if (file) onChange(file);
47+
};
48+
49+
const handleImportClick = () => {
50+
fileInputRef.current?.click();
51+
};
52+
const handleCopy = () => {
53+
if (value) {
54+
const blob = new Blob([value], { type: value.type });
55+
const clipboardItem = new ClipboardItem({ [value.type]: blob });
56+
57+
navigator.clipboard
58+
.write([clipboardItem])
59+
.then(() => showSnackBar('File copied', 'success'))
60+
.catch((err) => {
61+
showSnackBar('Failed to copy: ' + err, 'error');
62+
});
63+
}
64+
};
65+
66+
useEffect(() => {
67+
const handlePaste = (event: ClipboardEvent) => {
68+
const clipboardItems = event.clipboardData?.items ?? [];
69+
const item = clipboardItems[0];
70+
if (
71+
item &&
72+
(item.type.includes('image') || item.type.includes('video'))
73+
) {
74+
const file = item.getAsFile();
75+
if (file) onChange(file);
76+
}
77+
};
78+
window.addEventListener('paste', handlePaste);
79+
80+
return () => {
81+
window.removeEventListener('paste', handlePaste);
82+
};
83+
}, [onChange]);
84+
85+
return (
86+
<Box>
87+
<InputHeader
88+
title={title || 'Input ' + type.charAt(0).toUpperCase() + type.slice(1)}
89+
/>
90+
<Box
91+
sx={{
92+
width: '100%',
93+
height: globalInputHeight,
94+
border: preview ? 0 : 1,
95+
borderRadius: 2,
96+
boxShadow: '5',
97+
bgcolor: 'white',
98+
position: 'relative'
99+
}}
100+
>
101+
{preview ? (
102+
<Box
103+
width="100%"
104+
height="100%"
105+
sx={{
106+
display: 'flex',
107+
alignItems: 'center',
108+
justifyContent: 'center',
109+
backgroundImage: `url(${greyPattern})`,
110+
position: 'relative',
111+
overflow: 'hidden'
112+
}}
113+
>
114+
{children({ preview })}
115+
</Box>
116+
) : (
117+
<Box
118+
onClick={handleImportClick}
119+
sx={{
120+
display: 'flex',
121+
flexDirection: 'column',
122+
alignItems: 'center',
123+
justifyContent: 'center',
124+
padding: 5,
125+
height: '100%',
126+
cursor: 'pointer'
127+
}}
128+
>
129+
<Typography color={theme.palette.grey['600']}>
130+
Click here to select a {type} from your device, press Ctrl+V to
131+
use a {type} from your clipboard, drag and drop a file from
132+
desktop
133+
</Typography>
134+
</Box>
135+
)}
136+
</Box>
137+
<InputFooter handleCopy={handleCopy} handleImport={handleImportClick} />
138+
<input
139+
ref={fileInputRef}
140+
style={{ display: 'none' }}
141+
type="file"
142+
accept={accept.join(',')}
143+
onChange={handleFileChange}
144+
/>
145+
</Box>
146+
);
147+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import React, { useEffect, useRef, useState } from 'react';
2+
import { Box } from '@mui/material';
3+
import ReactCrop, { Crop, PixelCrop } from 'react-image-crop';
4+
import 'react-image-crop/dist/ReactCrop.css';
5+
import BaseFileInput from './BaseFileInput';
6+
import { BaseFileInputProps } from './file-input-utils';
7+
import { globalInputHeight } from '../../config/uiConfig';
8+
9+
interface ImageFileInputProps extends BaseFileInputProps {
10+
showCropOverlay?: boolean;
11+
cropShape?: 'rectangular' | 'circular';
12+
cropPosition?: { x: number; y: number };
13+
cropSize?: { width: number; height: number };
14+
onCropChange?: (
15+
position: { x: number; y: number },
16+
size: { width: number; height: number }
17+
) => void;
18+
}
19+
20+
export default function ToolImageInput({
21+
showCropOverlay = false,
22+
cropShape = 'rectangular',
23+
cropPosition = { x: 0, y: 0 },
24+
cropSize = { width: 100, height: 100 },
25+
onCropChange,
26+
...props
27+
}: ImageFileInputProps) {
28+
const imageRef = useRef<HTMLImageElement>(null);
29+
const [imgWidth, setImgWidth] = useState(0);
30+
const [imgHeight, setImgHeight] = useState(0);
31+
32+
const [crop, setCrop] = useState<Crop>({
33+
unit: 'px',
34+
x: 0,
35+
y: 0,
36+
width: 0,
37+
height: 0
38+
});
39+
40+
const RATIO = imageRef.current ? imgWidth / imageRef.current.width : 1;
41+
42+
const onImageLoad = (e: React.SyntheticEvent<HTMLImageElement>) => {
43+
const { naturalWidth: width, naturalHeight: height } = e.currentTarget;
44+
setImgWidth(width);
45+
setImgHeight(height);
46+
47+
if (!crop.width && !crop.height && onCropChange) {
48+
const initialCrop: Crop = {
49+
unit: 'px',
50+
x: Math.floor(width / 4),
51+
y: Math.floor(height / 4),
52+
width: Math.floor(width / 2),
53+
height: Math.floor(height / 2)
54+
};
55+
56+
setCrop(initialCrop);
57+
58+
onCropChange(
59+
{ x: initialCrop.x, y: initialCrop.y },
60+
{ width: initialCrop.width, height: initialCrop.height }
61+
);
62+
}
63+
};
64+
useEffect(() => {
65+
if (
66+
imgWidth &&
67+
imgHeight &&
68+
(cropPosition.x !== 0 ||
69+
cropPosition.y !== 0 ||
70+
cropSize.width !== 100 ||
71+
cropSize.height !== 100)
72+
) {
73+
setCrop({
74+
unit: 'px',
75+
x: cropPosition.x / RATIO,
76+
y: cropPosition.y / RATIO,
77+
width: cropSize.width / RATIO,
78+
height: cropSize.height / RATIO
79+
});
80+
}
81+
}, [cropPosition, cropSize, imgWidth, imgHeight, RATIO]);
82+
83+
const handleCropChange = (newCrop: Crop) => {
84+
setCrop(newCrop);
85+
};
86+
87+
const handleCropComplete = (crop: PixelCrop) => {
88+
if (onCropChange) {
89+
onCropChange(
90+
{ x: Math.round(crop.x * RATIO), y: Math.round(crop.y * RATIO) },
91+
{
92+
width: Math.round(crop.width * RATIO),
93+
height: Math.round(crop.height * RATIO)
94+
}
95+
);
96+
}
97+
};
98+
99+
return (
100+
<BaseFileInput {...props} type={'image'}>
101+
{({ preview }) => (
102+
<Box
103+
width="100%"
104+
height="100%"
105+
sx={{
106+
display: 'flex',
107+
alignItems: 'center',
108+
justifyContent: 'center',
109+
position: 'relative',
110+
overflow: 'hidden'
111+
}}
112+
>
113+
{showCropOverlay ? (
114+
<ReactCrop
115+
crop={crop}
116+
onChange={handleCropChange}
117+
onComplete={handleCropComplete}
118+
circularCrop={cropShape === 'circular'}
119+
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
120+
>
121+
<img
122+
ref={imageRef}
123+
src={preview}
124+
alt="Preview"
125+
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
126+
onLoad={onImageLoad}
127+
/>
128+
</ReactCrop>
129+
) : (
130+
<img
131+
ref={imageRef}
132+
src={preview}
133+
alt="Preview"
134+
style={{ maxWidth: '100%', maxHeight: globalInputHeight }}
135+
onLoad={onImageLoad}
136+
/>
137+
)}
138+
</Box>
139+
)}
140+
</BaseFileInput>
141+
);
142+
}

0 commit comments

Comments
 (0)