Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

15 create new campaign page #31

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions components/CreateNewCampaign/FileHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { Button, Grid } from '@mui/material';
import React from 'react';
lihuicham marked this conversation as resolved.
Show resolved Hide resolved

export interface FileHeaderProps {
file: File;
onDelete: (file: File) => void;
}

export function FileHeader({ file, onDelete }: FileHeaderProps) {
return (
<Grid container sx={styledGrid}>
<Grid item>{file.name}</Grid>
<Grid item>
<Button size="small" onClick={() => onDelete(file)}>
Delete
</Button>
</Grid>
</Grid>
);
}

const styledGrid = {
display: 'flex',
justifyContent: 'space-between',
alignItems: 'center'
}
121 changes: 121 additions & 0 deletions components/CreateNewCampaign/MultipleFileUploadField.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { Grid, Typography } from '@mui/material';
import { useField } from 'formik';
import React, { useCallback, useEffect, useState } from 'react';
import { FileError, FileRejection, useDropzone } from 'react-dropzone';
import SingleFileUploadWithProgress from './SingleFileUploadWithProgress';
import { UploadError } from './UploadError';

// import { makeStyles } from '@mui/styles';
import { createTheme, ThemeProvider } from '@mui/material/styles';

let currentId = 0;

function getNewId() {
return ++currentId;
}
Comment on lines +8 to +12
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not good to have a global variable. if we really need an id, put it as a state in the component.


export interface UploadableFile {
id: number;
file: File;
errors: FileError[];
url?: string;
}

const theme = createTheme();
lihuicham marked this conversation as resolved.
Show resolved Hide resolved

// const useStyles = makeStyles((theme) => ({
// dropzone: {
// border: `2px dashed ${theme.palette.primary.main}`,
// borderRadius: theme.shape.borderRadius,
// display: 'flex',
// alignItems: 'center',
// justifyContent: 'center',
// background: theme.palette.background.default,
// height: theme.spacing(10),
// outline: 'none',
// },
// }));

const styledTypo = {
textAlign: 'center',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
}


const MultipleFileUploadField = ({ name }: { name: string }) => {
const [_, __, helpers] = useField(name);
// const classes = useStyles();

const [files, setFiles] = useState<UploadableFile[]>([]);
const onDrop = useCallback((accFiles: File[], rejFiles: FileRejection[]) => {
const mappedAcc = accFiles.map((file) => ({ file, errors: [], id: getNewId() }));
const mappedRej = rejFiles.map((r) => ({ ...r, id: getNewId() }));
setFiles((curr) => [...curr, ...mappedAcc, ...mappedRej]);
Comment on lines +25 to +27
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

think we can do away with id and just use the file's name as the key for ur map below, and we should not allow file with same name to be uploaded.
https://stackoverflow.com/questions/26296232/dropzone-prevent-uploading-of-duplicate-files

}, []);

useEffect(() => {
helpers.setValue(files);
// helpers.setTouched(true);
}, [files]);

function onUpload(file: File, url: string) {
setFiles((curr) =>
curr.map((fw) => {
if (fw.file === file) {
return { ...fw, url };
}
return fw;
})
);
}

function onDelete(file: File) {
setFiles((curr) => curr.filter((fw) => fw.file !== file));
}

const { getRootProps, getInputProps } = useDropzone({
onDrop,
accept: {
'image/*': ['.png','.gif', '.jpeg', '.jpg', '.svg']
},
maxSize: 3000 * 1024, // 3MB
});

return (
<ThemeProvider theme={theme}>
<React.Fragment>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
<Grid item>
<div {...getRootProps()}>
<input {...getInputProps()} />
<Typography sx={styledTypo}>Click to upload or drag and drop</Typography>
<Typography sx={styledTypo}>PNG, GIF, JPEG, JPG or SVG files - max 3MB</Typography>
</div>
</Grid>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved

{files.map((fileWrapper) => (
<Grid item key={fileWrapper.id}>
{fileWrapper.errors.length ? (
<UploadError
file={fileWrapper.file}
errors={fileWrapper.errors}
onDelete={onDelete}
/>
) : (
<SingleFileUploadWithProgress
onDelete={onDelete}
onUpload={onUpload}
file={fileWrapper.file}
/>
)}
</Grid>
))}
</React.Fragment>
</ThemeProvider>

);
}


export default MultipleFileUploadField;
63 changes: 63 additions & 0 deletions components/CreateNewCampaign/SingleFileUploadWithProgress.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Grid, LinearProgress } from '@mui/material';
import React, { useEffect, useState } from 'react';
import { FileHeader } from './FileHeader';

export interface SingleFileUploadWithProgressProps {
file: File;
onDelete: (file: File) => void;
onUpload: (file: File, url: string) => void;
}

const SingleFileUploadWithProgress = ({
file,
onDelete,
onUpload,
}: SingleFileUploadWithProgressProps) => {
const [progress, setProgress] = useState(0);

useEffect(() => {
async function upload() {
const url = await uploadFile(file, setProgress);
onUpload(file, url);
}

upload();
}, []);

return (
<Grid item>
<FileHeader file={file} onDelete={onDelete} />
<LinearProgress variant="determinate" value={progress} />
</Grid>
);
}

function uploadFile(file: File, onProgress: (percentage: number) => void) {
const url = 'https://api.cloudinary.com/v1_1/demo/image/upload';
const key = 'docs_upload_example_us_preset';

return new Promise<string>((res, rej) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', url);

xhr.onload = () => {
const resp = JSON.parse(xhr.responseText);
res(resp.secure_url);
};
xhr.onerror = (evt) => rej(evt);
xhr.upload.onprogress = (event) => {
if (event.lengthComputable) {
const percentage = (event.loaded / event.total) * 100;
onProgress(Math.round(percentage));
}
};

const formData = new FormData();
formData.append('file', file);
formData.append('upload_preset', key);

xhr.send(formData);
});
}

export default SingleFileUploadWithProgress;
35 changes: 35 additions & 0 deletions components/CreateNewCampaign/UploadError.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import {
LinearProgress,
Typography,
} from '@mui/material';
import React from 'react';
import { FileError } from 'react-dropzone';
import { FileHeader } from './FileHeader';

export interface UploadErrorProps {
file: File;
onDelete: (file: File) => void;
errors: FileError[];
}

// const ErrorLinearProgress = withStyles((theme) =>
// createStyles({
// bar: {
// backgroundColor: theme.palette.error.main,
// },
// })
// )(LinearProgress);

export function UploadError({ file, onDelete, errors }: UploadErrorProps) {
return (
<React.Fragment>
<FileHeader file={file} onDelete={onDelete} />
<LinearProgress variant="buffer" value={100} color="error" />
{errors.map((error) => (
<div key={error.code}>
<Typography color="error">{error.message}</Typography>
</div>
))}
</React.Fragment>
);
}
51 changes: 51 additions & 0 deletions components/CreateNewCampaign/UploadMedia.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { Button, Card, CardContent, Grid } from '@mui/material';
import { Form, Formik } from 'formik';
import React from 'react';
import { array, object, string } from 'yup';
import MultipleFileUploadField from './MultipleFileUploadField';

const UploadMedia = () => {
return (
<Card>
<CardContent>
<Formik
initialValues={{ files: [] }}
validationSchema={object({
files: array(
object({
url: string().required(),
})
),
})}
onSubmit={(values) => {
console.log('values', values);
return new Promise((res) => setTimeout(res, 2000));
}}
>
{({isValid, isSubmitting }) => (
<Form>
<Grid container spacing={2} direction="column" sx={{height: '100%'}}>
<MultipleFileUploadField name="files" />

<Grid item>
<Button
variant="contained"
color="primary"
disabled={!isValid || isSubmitting}
type="submit"
sx={{marginTop: '10px'}}
>
Submit
</Button>
</Grid>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
</Grid>
</Form>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
)}
</Formik>
</CardContent>
</Card>
);
}

export default UploadMedia;

92 changes: 92 additions & 0 deletions components/CreateNewCampaign/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import * as React from 'react';
import { TextField } from "@mui/material";
import { Typography } from "@mui/material";
import { Box } from "@mui/system";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
import { DateTimePicker } from '@mui/x-date-pickers';
import { useState } from "react";
import UploadMedia from './UploadMedia';

const CreateNewCampaign = () => {
// const[value, setValue] = useState<DateRange<Date>>([null, null]);
const[startDate, setStartDate] = useState(null);
const[endDate, setEndDate] = useState(null);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that now we still just making the skeletal pages, but can already start considering how you are going to store the states for all the fields being entered. you can either have an object that stores the value of all fields or one state variable for each value. otherwise, if you are using Formik as mentioned below, there might be a different way that Formik can keep track of all the states. You should be implement all logic except for the step where we actually call API to send our form values


return (
<Box sx={styledMainBox}>
<Typography variant="h3" gutterBottom sx={styledCreateNewCampaign}>Create New Campaign</Typography>
<Typography variant="h4" gutterBottom sx={styledNameOfCampaign}>Name of campaign</Typography>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
<TextField id="outlined-basic" variant="outlined" placeholder="Enter name" />
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
<Typography variant="h4" gutterBottom sx={styledh4}>Message</Typography>
<TextField id="outlined-basic" variant="outlined" placeholder="Enter message" multiline rows={10}/>
<Typography variant="h4" gutterBottom sx={styledh4}>Duration</Typography>
<LocalizationProvider
dateAdapter={AdapterDateFns}
localeText={{ start: 'Start date', end: 'End date' }}
>
<Box sx={styledDateBox}>
<DateTimePicker
renderInput={(props) => <TextField {...props} />}
label="Start date"
disablePast
value={startDate}
onChange={(newValue) => {
setStartDate(newValue);
}}
/>
lihuicham marked this conversation as resolved.
Show resolved Hide resolved
<DateTimePicker
renderInput={(props) => <TextField {...props} />}
label="End date"
disablePast
value={endDate}
onChange={(newValue) => {
setEndDate(newValue);
}}
/>
</Box>
<Typography variant="h4" gutterBottom sx={styledh4}>Upload Media</Typography>
</LocalizationProvider>
<UploadMedia />
</Box>
)
}
export default CreateNewCampaign;

const styledMainBox = {
display: 'flex',
flexDirection: 'column',
width: '100%',
height: '100%',
padding: '10px',
backgroundColor: '',
}

const styledCreateNewCampaign = {
display: 'flex',
alignItems: 'center',
textAlign: 'center',
fontWeight: '700',
fontSize: '28px',
lineHeight: '40px',
}

const styledNameOfCampaign = {
fontWeight: '700',
fontSize: '21px',
lineHeight: '40px',
}


const styledh4 = {
fontWeight: '700',
fontSize: '21px',
lineHeight: '40px',
marginTop: '1rem'
}

const styledDateBox = {
display: 'flex',
gap: '30px',
}

Loading