<div id="root"></div>
@import url('https://fonts.googleapis.com/css2?family=Chakra+Petch:wght@500&display=swap');

* {
	font-family: 'Chakra Petch', sans-serif;
}
body, h1, h2, h3, h4, h5, h6 {
  margin: 0;
}
const { useState, useCallback } = React;

const FONT_SIZES = {
	xs: ".8vmin",
	sm: "1.2vmin",
	md: "1.8vmin",
	lg: "2vmin"
};

const MARGIN = {
	xs: ".8vmin",
	sm: "1.4vmin",
	md: "1.8vmin",
	lg: "3vmin"
};

const COLORS = {
	background: "#171734",
	white: "#ffffff",
	accent: "#ffc107",
	bright: "#ffffff",
	dark: "#333333",
	gray: "#555555",
	lightGray: "#bbbbbb",
	superLightGray: "#dedede",
	red: "#dc3545"
};

const BUTTON_THEMES = {
	default: {
		background: COLORS.lightGray,
		color: COLORS.dark
	},
	primary: {
		background: COLORS.accent,
		color: COLORS.dark
	},
	danger: {
		background: COLORS.red,
		color: COLORS.white
	}
};

const BUTTON_SIZES = {
	sm: {
		fontSize: FONT_SIZES.sm,
		padding: ".6vmin 1.6vmin"
	},
	md: {
		fontSize: FONT_SIZES.md,
		padding: "1vmin 2vmin"
	},
	lg: {
		fontSize: FONT_SIZES.lg,
		padding: "1.4vmin 2.4vmin"
	}
};

const LOCAL_IMAGE_PREVIEWER = {
	height: "50vmin",
	width: "80vmin"
};

const IMAGE_SIZE_LIMIT = 500;

const useLocalImagePreviewer = (sizeLimit) => {
	const [localImage, setLocalImage] = useState(null);
	
	const getLocalFileDataURL = (fileData) => {
		return new Promise((resolve) => {
			const reader = new FileReader();
			console.log(reader)
			reader.readAsDataURL(fileData);

			reader.addEventListener("load", (e) => {
				console.log(e);
				const loadedFile = e.target.result;
				resolve(loadedFile);
			});
		});
	};
	
	const handleChange = useCallback(async (e) => {
		console.log(e.target.files)
		const fileData = e.target.files[0];

		if (!fileData) return; //エクスプローラでキャンセルしたときにエラーが発生しないように

		if (!fileData.type.match("image.*")) {
			alert("Please select image");
			return;
		}

		if (sizeLimit) {
			if (fileData.size > sizeLimit * 1000) {
				alert(`File size should be ${sizeLimit}KB or less.`);
				return;
			}
		}

		const loadImageURL = await getLocalFileDataURL(fileData);
	
		setLocalImage(loadImageURL);
	}, []);

	const clearFilePath = useCallback((e) => {
		e.target.value = null;
	}, []);

	const removeLocalImage = useCallback(() => setLocalImage(null), []);

	const handleDrop = useCallback(async (e) => {
		e.stopPropagation();
		e.preventDefault();

		const files = e.dataTransfer.files; // FileList
		const fileData = files[0]; // File

		if (!fileData.type.match("image.*")) {
			alert("Please select image");
			return;
		}

		if (sizeLimit) {
			if (fileData.size > sizeLimit * 1000) {
				alert(`File size should be ${sizeLimit}KB or less.`);
				return;
			}
		}

		const loadImageURL = await getLocalFileDataURL(fileData);
		setLocalImage(loadImageURL);
	});

	const handleDragOver = useCallback((e) => {
		e.stopPropagation();
		e.preventDefault();
		e.dataTransfer.dropEffect = "copy";
	}, []);

	return {
		localImage,
		handleChange,
		clearFilePath,
		removeLocalImage,
		handleDragOver,
		handleDrop
	};
};

const FlexAllCenterStyle = styled.css`
	display: flex;
	align-items: center;
	justify-content: center;
`;

const Button = styled.button`
	background-color: ${({ theme }) =>
		(theme && theme.background) || BUTTON_THEMES.default.background};
	border: none;
	border-radius: 0.6vmin;
	cursor: ${({ isDisabled }) => (isDisabled ? "cursor" : "pointer")};
	color: ${({ theme }) => (theme && theme.color) || BUTTON_THEMES.default.color};
	font-size: ${({ size }) =>
		(size && size.fontSize) || BUTTON_SIZES.md.fontSize};
	//margin-top: ${({ margin }) => (margin && margin.top) || 0};
	//margin-right: ${({ margin }) => (margin && margin.right) || 0};
	//margin-bottom: ${({ margin }) => (margin && margin.bottom) || 0};
	//margin-left: ${({ margin }) => (margin && margin.left) || 0};
	outline: none;
	opacity: ${({ isDisabled }) => (isDisabled ? 0.5 : 1)};
	padding: ${({ size }) => (size && size.padding) || BUTTON_SIZES.md.padding};
	pointer-events: ${({ isDisabled }) => (isDisabled ? "none" : "auto")};
	user-select: none;
`;

const StyledLocalFileSelectButton = Button.withComponent("label");

const Input = styled.input`
	display: none;
`;

const StyledLocalImagePreviewer = styled.div`
	background-color: ${({ background }) => background || COLORS.superLightGray};
	border-radius: 1vmin;
	box-sizing: border-box;
	height: ${({ height }) => height};
	padding: 2vmin;
	width: ${({ width }) => width};
`;

const LocalImage = styled.img`
	max-height: 100%;
	width: auto;
	//position: absolute;
	//top: 0;
`;

const DragZone = styled.div`
	border-color: ${({ borderColor }) => borderColor || COLORS.lightGray};
	border-width: 0.4vmin;
	border-style: dashed;
	box-sizing: border-box;
	${FlexAllCenterStyle};
	flex-direction: column;
	overflow: hidden;
	//position: relative;
	height: 100%;
	width: 100%;
`;

const Margin = styled.div`
	margin-top: ${({ top }) => top || 0};
	margin-right: ${({ right }) => right || 0};
	margin-bottom: ${({ bottom }) => bottom || 0};
	margin-left: ${({ left }) => left || 0};
	width: 100%;
`;

const Message = styled.h3`
	color: ${COLORS.dark};
	font-size: ${FONT_SIZES.md};
`;

const LocalFileSelectButton = ({
	className,
	theme,
	size,
	clearFilePath,
	handleChange,
	accept,
	children
}) => (
	<StyledLocalFileSelectButton
		htmlFor="file"
		className={className}
		theme={theme}
		size={size}
		onClick={clearFilePath}
		onChange={handleChange}
	>
		<Input
			type="file"
			id="file"
			name="file"
			className="file"
			accept={accept}
		/>
		{children}
	</StyledLocalFileSelectButton>
);

const StyledApp = styled.div`
	background-color: ${COLORS.background}; //cornflowerblue;
	${FlexAllCenterStyle};
	height: 100vh;
	width: 100%;
`;

const Container = styled.div`
	${FlexAllCenterStyle};
	flex-direction: column;
	padding: 4vmin;
	width: 100%;
`;

const LocalImagePreviewer = ({
	localImagePreviewer,
	height,
	width,
	background
}) => (
	<React.Fragment>
		<StyledLocalImagePreviewer
			height={height}
			width={width}
			background={background}
		>
			<DragZone
				onDragOver={localImagePreviewer.handleDragOver}
				onDrop={localImagePreviewer.handleDrop}
				borderColor={background}
			>		
				{
					localImagePreviewer.localImage ? 
						<LocalImage src={localImagePreviewer.localImage} />
				 	: 
						<React.Fragment>
							<Message>Drag and drop a file to upload...</Message>
							<Margin top="1.4vmin" />
							<LocalFileSelectButton
								theme={BUTTON_THEMES.primary}
								handleChange={localImagePreviewer.handleChange}
								clearFilePath={localImagePreviewer.clearFilePath}
								accept=".png, .jpg, .jpeg"
							>
								Open file Selector
							</LocalFileSelectButton>
						</React.Fragment>
				}
			</DragZone>
		</StyledLocalImagePreviewer>
		<Margin bottom="3vmin" />
		<Button
			onClick={localImagePreviewer.removeLocalImage}
			isDisabled={!localImagePreviewer.localImage}
			theme={BUTTON_THEMES.danger}
		>
			Delete file
		</Button>
	</React.Fragment>
);

const App = () => {
	const localImagePreviewer = useLocalImagePreviewer(IMAGE_SIZE_LIMIT);

	return (
		<StyledApp>
			<Container>
				<LocalImagePreviewer
					localImagePreviewer={localImagePreviewer}
					height={LOCAL_IMAGE_PREVIEWER.height}
					width={LOCAL_IMAGE_PREVIEWER.width}
				/>
			</Container>
		</StyledApp>
	);
};

ReactDOM.render(<App />, document.getElementById("root"));
View Compiled
Run Pen

External CSS

This Pen doesn't use any external CSS resources.

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js
  2. https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js
  3. https://cdnjs.cloudflare.com/ajax/libs/styled-components/4.3.1/styled-components.min.js