Client side picture upload

This commit is contained in:
Daniel Cojocea committed 2024-06-26 19:06:10 -04:00
1 parent 74da41bf3b
commit 95a30c5a8d
6 files changed
+310 -122

No files matched your search

@@ -0,0 +1,18 @@
.progress-bar-container{
background-color: #fafafa;
padding: var(--env-var-spacing-1-plus);
}
.progress-bar-container h2.MuiTypography-root,
.progress-bar-container p.MuiTypography-root {
color: var(--env-var-color-2);
font-size: var(--env-var-font-size-medium);
}
.progress-bar-container p.MuiTypography-root {
opacity: 0.6;
}
.progress-bar-container p.MuiTypography-root:has(span){
font-size: 12px;
}
.progress-bar-container p.MuiTypography-root span {
padding-left: 2px;
}
@@ -0,0 +1,104 @@
import React, { useState } from "react";
import { useTheme } from "@emotion/react";
import {
Box,
IconButton,
LinearProgress,
Stack,
Typography,
} from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import "./index.css";
/**
* @param {Object} props - The component props.
* @returns {JSX.Element} The rendered component.
*/
const ProgressUpload = ({
icon,
label,
size,
progress = 0,
onClick,
}) => {
const theme = useTheme();
return (
<Box
className="progress-bar-container"
mt="20px"
sx={{
minWidth: "200px",
height: "fit-content",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid 1px ${theme.palette.otherColors.graishWhite}`,
}}
>
<Stack direction="row" mb="10px" gap="10px" alignItems="flex-end">
{icon ? (
<IconButton
sx={{
backgroundColor: theme.palette.otherColors.white,
pointerEvents: "none",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid ${theme.shape.borderThick}px ${theme.palette.otherColors.graishWhite}`,
boxShadow: theme.shape.boxShadow,
}}
>
{icon}
</IconButton>
) : (
""
)}
<Box>
<Typography variant="h4" component="h2" mb="5px">
{label}
</Typography>
<Typography variant="h4" component="p">
{size}
</Typography>
</Box>
<IconButton
onClick={onClick}
sx={{
alignSelf: "flex-start",
ml: "auto",
mr: "-5px",
mt: "-5px",
padding: "5px",
"&:focus": {
outline: "none",
},
}}
>
<CloseIcon
sx={{
fontSize: "20px",
}}
/>
</IconButton>
</Stack>
<Stack direction="row" alignItems="center">
<Box sx={{ width: "100%", mr: "10px" }}>
<LinearProgress
variant="determinate"
value={progress}
sx={{
width: "100%",
height: "10px",
borderRadius: theme.shape.borderRadius,
maxWidth: "500px",
backgroundColor: theme.palette.otherColors.graishWhite,
}}
/>
</Box>
<Typography variant="h4" component="p" sx={{ minWidth: "max-content" }}>
{progress}
<span>%</span>
</Typography>
</Stack>
</Box>
);
};
export default ProgressUpload;
@@ -1,14 +1,7 @@
import { useTheme } from "@emotion/react";
import { useState } from "react";
import TabPanel from "@mui/lab/TabPanel";
import {
Box,
Divider,
IconButton,
Modal,
Stack,
Typography,
} from "@mui/material";
import { Box, Divider, Modal, Stack, Typography } from "@mui/material";
import ButtonSpinner from "../../ButtonSpinner";
import Button from "../../Button";
import EmailTextField from "../../TextFields/Email/EmailTextField";
@@ -18,8 +11,8 @@ import { editProfileValidation } from "../../../Validation/validation";
import { useDispatch, useSelector } from "react-redux";
import { update } from "../../../Features/Auth/authSlice";
import ImageField from "../../TextFields/Image";
import DoDisturbAltIcon from "@mui/icons-material/DoDisturbAlt";
import CloseIcon from "@mui/icons-material/Close";
import ImageIcon from "@mui/icons-material/Image";
import ProgressUpload from "../../ProgressBars";
/**
* ProfilePanel component displays a form for editing user profile information
@@ -78,15 +71,64 @@ const ProfilePanel = () => {
});
};
//TODO - implement delete profile picture function
//TODO - add setPicture state to localData
const [picture, setPicture] = useState();
const [uploadProgress, setUploadProgress] = useState(0);
const [isUploading, setIsUploading] = useState(false);
const handlePicture = (event) => {
const pic = event.target.files[0];
console.log(pic);
//TODO - add setPicture state to localData
setPicture({ name: pic.name, type: pic.type, size: formatBytes(pic.size) });
setIsUploading(true);
setTimeout(() => {
//TODO - add setPicture state to localData
setPicture((prev) => ({ ...prev, src: URL.createObjectURL(pic) }));
}, 1500);
let progress = 0;
const interval = setInterval(() => {
progress += 10;
setUploadProgress(progress);
if (progress >= 100) {
clearInterval(interval);
}
}, 150);
};
const formatBytes = (bytes) => {
if (bytes === 0) return "0 Bytes";
const k = 1024;
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " MB";
};
const handleCancelUpload = () => {
//TODO - add setPicture state to localData
setPicture(null);
setUploadProgress(0);
setIsUploading(false);
};
const handleClosePictureModal = () => {
setIsOpen("");
setUploadProgress(0);
setIsUploading(false);
};
//TODO - revisit once localData is set up properly
const handleDeletePicture = () => {
setLoading(true);
setTimeout(() => {
//TODO - add setPicture state to localData
setPicture(null);
setLoading(false);
}, 2000);
};
//TODO - implement update profile function
const handleUpdatePicture = () => {};
const handleUpdatePicture = () => {
setIsOpen("");
setUploadProgress(0);
setIsUploading(false);
};
//TODO - implement delete account function
const handleDeleteAccount = () => {};
//TODO - implement save profile function
@@ -199,7 +241,7 @@ const ProfilePanel = () => {
<Stack className="row-stack" direction="row" alignItems="center">
{/* TODO - Update placeholder values with redux data */}
<Avatar
src="/static/images/avatar/2.jpg"
src={picture ? picture.src : "/static/images/avatar/2.jpg"}
firstName={localData.firstname}
lastName={localData.lastname}
sx={{
@@ -215,7 +257,6 @@ const ProfilePanel = () => {
onClick={handleDeletePicture}
isLoading={loading}
/>
{/* TODO - modal popup for update pfp? */}
<Button
level="tertiary"
label="Update"
@@ -326,11 +367,10 @@ const ProfilePanel = () => {
aria-labelledby="modal-update-picture"
aria-describedby="update-profile-picture"
open={isModalOpen("picture")}
onClose={() => setIsOpen("")}
onClose={handleClosePictureModal}
disablePortal
>
<Stack
gap="20px"
sx={{
position: "absolute",
top: "50%",
@@ -347,43 +387,44 @@ const ProfilePanel = () => {
},
}}
>
<Stack
direction="row"
mt="-10px"
justifyContent="space-between"
alignItems="center"
>
<Typography id="modal-update-picture" variant="h4" component="h1">
Upload Image
</Typography>
<IconButton
onClick={() => setIsOpen("")}
sx={{
"&:focus": {
outline: "none",
},
}}
>
<CloseIcon />
</IconButton>
</Stack>
<ImageField id="update-profile-picture" />
<Stack direction="row" mt="10px" justifyContent="space-between">
<Button level="secondary" label="Edit" disabled />
<Stack direction="row" gap="10px" justifyContent="flex-end">
<ButtonSpinner
level="tertiary"
label="Remove"
onClick={handleDeletePicture}
isLoading={loading}
/>
<ButtonSpinner
level="primary"
label="Update"
onClick={handleUpdatePicture}
isLoading={isLoading}
/>
</Stack>
<Typography id="modal-update-picture" variant="h4" component="h1">
Upload Image
</Typography>
<ImageField
id="update-profile-picture"
picture={picture?.src}
onChange={handlePicture}
/>
{isUploading ? (
<ProgressUpload
icon={<ImageIcon />}
label={picture?.name}
size={picture?.size}
progress={uploadProgress}
onClick={handleCancelUpload}
/>
) : (
""
)}
<Stack direction="row" mt="20px" gap="10px" justifyContent="flex-end">
<Button
level="secondary"
label="Edit"
disabled
sx={{ mr: "auto" }}
/>
<ButtonSpinner
level="tertiary"
label="Remove"
onClick={handleCancelUpload}
isLoading={loading}
/>
<ButtonSpinner
level="primary"
label="Update"
onClick={handleUpdatePicture}
isLoading={isLoading}
/>
</Stack>
</Stack>
</Modal>
@@ -8,7 +8,7 @@ import "./index.css";
* @param {Object} props - The component props.
* @returns {JSX.Element} The rendered component.
*/
const ImageField = ({ id, onChange }) => {
const ImageField = ({ id, picture, onChange }) => {
const theme = useTheme();
const [isDragging, setIsDragging] = useState(false);
@@ -20,77 +20,95 @@ const ImageField = ({ id, onChange }) => {
};
return (
<Box
className="image-field-wrapper"
sx={{
position: "relative",
width: "fit-content",
height: "fit-content",
border: "dashed",
borderRadius: theme.shape.borderRadius / 2,
borderSpacing: "10px",
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.otherColors.graishWhite,
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDragLeave}
>
<TextField
id={id}
type="file"
sx={{
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
padding: 10,
maxHeight: "300px",
height: "100%",
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
className="custom-file-text"
alignItems="center"
gap="10px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: "-1",
width: "100%",
}}
>
<IconButton
<>
{!picture ? (
<Box
className="image-field-wrapper"
mt="20px"
sx={{
pointerEvents: "none",
borderRadius: "2px",
border: `solid 2px ${theme.palette.otherColors.graishWhite}`,
boxShadow: theme.shape.boxShadow,
position: "relative",
height: "fit-content",
border: "dashed",
borderRadius: `${theme.shape.borderRadius}px`,
borderColor: isDragging
? theme.palette.primary.main
: theme.palette.otherColors.graishWhite,
borderWidth: "2px",
transition: "0.2s",
"&:hover": {
borderColor: theme.palette.primary.main,
backgroundColor: "hsl(215, 87%, 51%, 0.05)",
},
}}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDrop={handleDragLeave}
>
<CloudUploadIcon />
</IconButton>
<Typography variant="h4" component="h2">
<span>Click to upload</span> or drag and drop
</Typography>
<Typography variant="h4" component="p">
(max. 800x400px)
</Typography>
</Stack>
</Box>
<TextField
id={id}
type="file"
onChange={onChange}
sx={{
width: "100%",
"& .MuiInputBase-input[type='file']": {
opacity: 0,
cursor: "pointer",
maxWidth: "500px",
minHeight: "175px"
},
"& fieldset": {
padding: 0,
border: "none",
},
}}
/>
<Stack
className="custom-file-text"
alignItems="center"
gap="10px"
sx={{
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
zIndex: "-1",
width: "100%",
}}
>
<IconButton
sx={{
pointerEvents: "none",
borderRadius: `${theme.shape.borderRadius}px`,
border: `solid ${theme.shape.borderThick}px ${theme.palette.otherColors.graishWhite}`,
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
</IconButton>
<Typography variant="h4" component="h2">
<span>Click to upload</span> or drag and drop
</Typography>
<Typography variant="h4" component="p">
(max. 800x400px)
</Typography>
</Stack>
</Box>
) : (
<Stack direction="row" justifyContent="center">
<Box
sx={{
width: "250px",
height: "250px",
borderRadius: "50%",
overflow: "hidden",
backgroundImage: `url(${picture})`,
backgroundSize: "cover"
}}
>
</Box>
</Stack>
)}
</>
);
};
+6
View File
@@ -29,6 +29,7 @@ import AddIcon from "@mui/icons-material/Add";
import Divider from "@mui/material/Divider";
import UploadIcon from "@mui/icons-material/Upload";
import SendIcon from "@mui/icons-material/Send";
import ImageIcon from '@mui/icons-material/Image';
// Redux
import { useSelector } from "react-redux";
@@ -36,6 +37,7 @@ import { useDispatch } from "react-redux";
import { getMonitors } from "../../Features/Monitors/monitorsSlice";
import { getMonitorsByUserId } from "../../Features/Monitors/monitorsSlice";
import ImageField from "../../Components/TextFields/Image";
import ProgressUpload from "../../Components/ProgressBars";
const cols = [
{
@@ -420,6 +422,10 @@ const Demo = () => {
<ImageField />
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
<Stack justifyContent="center" alignItems="center">
<ProgressUpload icon={<ImageIcon />} label="image.jpg" size="2 MB"/>
</Stack>
<Divider sx={{ margin: `${theme.spacing(2)}` }} />
</div>
);
};
+1
View File
@@ -83,6 +83,7 @@ const theme = createTheme({
},
shape: {
borderRadius: 4,
borderThick: 2,
boxShadow: shadow,
},
});