Client side picture upload
This commit is contained in:
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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -83,6 +83,7 @@ const theme = createTheme({
|
||||
},
|
||||
shape: {
|
||||
borderRadius: 4,
|
||||
borderThick: 2,
|
||||
boxShadow: shadow,
|
||||
},
|
||||
});
|
||||
|
||||
Reference in new issue
Block a user