Skip to content

Commit

Permalink
feat(create secret) add file upload support (#11)
Browse files Browse the repository at this point in the history
  • Loading branch information
elysee15 authored Aug 25, 2021
1 parent 194ef12 commit 652cf39
Show file tree
Hide file tree
Showing 20 changed files with 492 additions and 103 deletions.
1 change: 1 addition & 0 deletions web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"react": "^17.0.2",
"react-copy-to-clipboard": "^5.0.3",
"react-dom": "^17.0.2",
"react-dropzone": "^11.3.4",
"react-router-dom": "^5.2.0",
"react-scripts": "4.0.3",
"react-select": "^4.3.1",
Expand Down
2 changes: 1 addition & 1 deletion web/src/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { render } from "@testing-library/react";
import { render } from "utils/test-utils";
import App from "./App";

test("renders app component", () => {
Expand Down
52 changes: 23 additions & 29 deletions web/src/components/CreateSecretForm.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Button, Typography } from "@material-ui/core";
import { Typography } from "@material-ui/core";
import { TextField, Switch } from "formik-material-ui";
import { Field, Form, Formik } from "formik";
import { Autocomplete, AutocompleteRenderInputParams } from "formik-material-ui-lab";
Expand All @@ -8,35 +8,19 @@ import React, { useEffect } from "react";
import { CreateSecretFormProps } from "types/CreateSecretFormProps";
import { preventNonNumericalInput } from "utils/utils";
import { useStyles } from "styles/createSecretFormStyles";
import * as Yup from "yup";

const options: Lifetime[] = [
{ value: "5m", label: "5 min" },
{ value: "15m", label: "15 min" },
{ value: "30m", label: "30 min" },
{ value: "1h", label: "1 hour" },
{ value: "2h", label: "2 hours" },
{ value: "3h", label: "3 hours" },
{ value: "24h", label: "1 day" },
{ value: "48h", label: "2 days" },
{ value: "72h", label: "3 days" },
{ value: "168h", label: "7 days" }
];

const CreateSecretSchema = Yup.object().shape({
secret: Yup.string().required("You must add a secret"),
password: Yup.string(),
lifetime: Yup.object().shape({ value: Yup.string(), label: Yup.string() }).nullable(),
accessType: Yup.boolean(),
accesses: Yup.number().max(108, "The max number is 108")
});
import clsx from "clsx";
import Button from "./Button";
import StyledDropzone from "./Dropzone";
import { CreateSecretFileSchema, CreateSecretMessageSchema } from "utils/validation-schema";
import { LIFETIME_OPTIONS } from "constants/index";

const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
const classes = useStyles();
const CreateSecretSchema = props.type === "file" ? CreateSecretFileSchema : CreateSecretMessageSchema;

return (
<Formik initialValues={props.initialValues} validationSchema={CreateSecretSchema} onSubmit={props.onSubmit}>
{({ isSubmitting, errors, values, setFieldValue }) => {
{({ errors, values, setFieldValue }) => {
useEffect(() => {
if (values.accessType) {
setFieldValue("accesses", -1);
Expand All @@ -47,7 +31,7 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {

return (
<Form className={classes.form} noValidate>
<div>
<div className={clsx({ [classes.hide]: props.type === "file" })}>
<Field
component={TextField}
name="secret"
Expand All @@ -62,6 +46,10 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
maxRows={15}
/>
</div>
<div className={clsx({ [classes.hide]: props.type === "message" })}>
<Field component={StyledDropzone} />
</div>

<div>
<Field
component={TextField}
Expand Down Expand Up @@ -97,7 +85,7 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
name="lifetime"
component={Autocomplete}
size="small"
options={options}
options={LIFETIME_OPTIONS}
getOptionLabel={(option: Lifetime) => option.label}
getOptionSelected={(option: Lifetime, value: Lifetime) => option.value === value.value}
renderInput={(params: AutocompleteRenderInputParams) => (
Expand All @@ -116,9 +104,15 @@ const CreateSecretForm: React.FC<CreateSecretFormProps> = props => {
/>
</div>
<div>
<Button type="submit" variant="contained" color="primary" disabled={isSubmitting}>
Get Token
</Button>
<Button
label="Get Token"
type="submit"
variant="contained"
color="primary"
fullWidth
isLoading={props.loading}
disabled={props.loading}
/>
</div>
</Form>
);
Expand Down
87 changes: 87 additions & 0 deletions web/src/components/CreateSecretFormTabs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import React from "react";
import { makeStyles, Theme } from "@material-ui/core/styles";
import Tabs from "@material-ui/core/Tabs";
import Tab from "@material-ui/core/Tab";
import TabPanel from "./TabPanel";
import CreateSecretForm from "./CreateSecretForm";
import { FormikHelpers, FormikValues } from "formik";
import { Box, Divider, Typography } from "@material-ui/core";

type SecretType = "file" | "message";

function a11yProps(index: any) {
return {
id: `form-tab-${index}`,
"aria-controls": `simple-tabpanel-${index}`
};
}

const useStyles = makeStyles((theme: Theme) => ({
root: {
maxWidth: "500px",
margin: "0 auto",
backgroundColor: theme.palette.background.paper,
gap: theme.spacing(4),
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
width: "100%",
maxHeight: "80%",
height: "80%",
boxShadow: "rgba(0, 0, 0, 0.1) 0px 4px 12px",
background: "#f5f7f8",
borderRadius: "5px",
padding: theme.spacing(2)
},
container: {}
}));

type CreateSecretFormTabsProps = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
onSubmit: (values: FormikValues, helpers: FormikHelpers<any>) => void;
initialValues: FormikValues;
loading: boolean;
};

const CreateSecretFormTabs: React.FC<CreateSecretFormTabsProps> = props => {
const classes = useStyles();
const [value, setValue] = React.useState<"file" | "message">("message");

const handleChange = (event: React.ChangeEvent<any>, newValue: SecretType) => {
setValue(newValue);
};

return (
<div className={classes.root}>
<Box textAlign="center" marginY="1rem">
<Typography variant="body1" style={{ fontWeight: "bold" }}>
Which kind of secret do you want to share ?
</Typography>
</Box>
<Divider style={{ margin: "0 3rem" }} />
<Tabs value={value} onChange={handleChange} centered>
<Tab value="message" label="Secret message" {...a11yProps("message")} />
<Tab value="file" label="Secret file" {...a11yProps("file")} />
</Tabs>
<TabPanel value={value} index="message">
<CreateSecretForm
type={value}
onSubmit={props.onSubmit}
initialValues={props.initialValues}
loading={props.loading}
/>
</TabPanel>
<TabPanel value={value} index="file">
<CreateSecretForm
type={value}
onSubmit={props.onSubmit}
initialValues={props.initialValues}
loading={props.loading}
/>
</TabPanel>
</div>
);
};

export default CreateSecretFormTabs;
85 changes: 85 additions & 0 deletions web/src/components/Dropzone.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { Typography } from "@material-ui/core";
import clsx from "clsx";
import { makeStyles } from "@material-ui/styles";
import { ErrorMessage, FieldProps } from "formik";
import { useMemo } from "react";
import { useDropzone } from "react-dropzone";

const useStyles = makeStyles(() => ({
error: {
borderColor: "#f44336 !important"
}
}));

const baseStyle = {
flex: 1,
display: "flex",
alignItems: "center",
padding: "20px",
borderWidth: 2,
borderRadius: 2,
height: "175px",
borderColor: "#eeeeee",
borderStyle: "dashed",
backgroundColor: "#fafafa",
color: "#bdbdbd",
outline: "none",
transition: "border .24s ease-in-out"
};

const activeStyle = {
borderColor: "#2196f3"
};

const acceptStyle = {
borderColor: "#00e676"
};

type StyledDropzoneProps = FieldProps;

const FILE_SIZE = 64 * 1024;

const StyledDropzone: React.FC<StyledDropzoneProps> = ({ form }) => {
const classes = useStyles();
const { getRootProps, getInputProps, acceptedFiles, isDragActive, isDragAccept } = useDropzone({
maxSize: FILE_SIZE,
onDrop: (_acceptedFiles: File[]) => {
const file = _acceptedFiles[0];
form.setFieldValue("file", file);
form.setFieldValue("filename", file.name);
form.setFieldValue("is_base64", true);
}
});

const files = acceptedFiles.map(file => <Typography key={file.name}>{file.name}</Typography>);

const style = useMemo(
() => ({
...baseStyle,
...(isDragActive ? activeStyle : {}),
...(isDragAccept ? acceptStyle : {})
}),
[isDragActive, isDragAccept]
);

return (
<div className="container">
<div
{...getRootProps({ style })}
className={clsx({ [classes.error]: form.touched["file"] && !!form.errors["file"] })}
>
<input {...getInputProps()} />
{isDragActive ? (
<p>Drop the files here ...</p>
) : (
<>{!files.length ? "Drag'n drop your file here, or click to select the file" : files}</>
)}
</div>
<small className="MuiFormHelperText-root MuiFormHelperText-contained Mui-error Mui-required">
<ErrorMessage name="file" />
</small>
</div>
);
};

export default StyledDropzone;
11 changes: 10 additions & 1 deletion web/src/components/Layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import { makeStyles } from "@material-ui/core";
import Footer from "./Footer";

type LayoutProps = {
children: React.ReactNode;
};

const useStyles = makeStyles(() => ({
children: {
height: "100vh",
width: "100%"
}
}));

const Layout: React.FC<LayoutProps> = ({ children }) => {
const classes = useStyles();
return (
<div>
{children}
<div className={classes.children}>{children}</div>
<Footer />
</div>
);
Expand Down
63 changes: 63 additions & 0 deletions web/src/components/ShowFile.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import React from "react";
import AttachmentIcon from "@material-ui/icons/Attachment";
import { Box, Button, makeStyles, Theme, Typography } from "@material-ui/core";

const useStyles = makeStyles((theme: Theme) => ({
container: {
position: "absolute",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
width: "100%",
maxWidth: "500px",
background: "#f7f5f5",
padding: theme.spacing(2)
}
}));

type ShowFileProps = {
file?: File;
uploadedAt?: Date;
};

const ShowFile: React.FC<ShowFileProps> = ({ file }) => {
const classes = useStyles();

const handleDownloadClick = () => {
if (file) {
const url = window.URL.createObjectURL(new Blob([file]));
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.setAttribute("download", file.name);
document.body.appendChild(link);
link.click();
link.parentNode && link.parentNode.removeChild(link);
}
};

return (
<div className={classes.container}>
<Box display="flex" alignItems="center" justifyContent="space-between">
<div>
<AttachmentIcon fontSize="large" />
</div>
<div>
<Typography variant="caption">Name</Typography>
<Typography>{file?.name}</Typography>
</div>
<div>
<Typography variant="caption">Size</Typography>
<Typography>{`${file?.size && (file?.size / 1024).toFixed(2)} Mb`}</Typography>
</div>
<div>
<Button variant="contained" color="primary" onClick={handleDownloadClick} fullWidth>
Download
</Button>
</div>
</Box>
</div>
);
};

export default ShowFile;
Loading

0 comments on commit 652cf39

Please sign in to comment.