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

140 project submit page #153

Merged
merged 10 commits into from
May 12, 2024
Merged
191 changes: 191 additions & 0 deletions frontend/app/[locale]/components/SubmitDetailsPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
"use client"

import React, {useEffect, useState} from 'react';
import {useTranslation} from "react-i18next";
import {
Box,
Button,
Card,
CardContent,
Divider,
Grid,
IconButton,
Input,
LinearProgress,
ThemeProvider,
Typography
} from "@mui/material";
import {getProject, Project, uploadSubmissionFile} from '@lib/api';
import baseTheme from "@styles/theme";
import ProjectReturnButton from "@app/[locale]/components/ProjectReturnButton";
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import ExpandLessIcon from '@mui/icons-material/ExpandLess';
import PublishIcon from '@mui/icons-material/Publish';
import CheckCircleIcon from '@mui/icons-material/CheckCircle';
import ErrorIcon from '@mui/icons-material/Error';
import Tree from "@app/[locale]/components/Tree";

interface SubmitDetailsPageProps {
locale: any;
project_id: number;
}

const SubmitDetailsPage: React.FC<SubmitDetailsPageProps> = ({locale, project_id}) => {
const {t} = useTranslation();

const [projectData, setProjectData] = useState<Project>()
const [paths, setPaths] = useState<string[]>([]);
const [submitted, setSubmitted] = useState<string>("no");
const [loadingProject, setLoadingProject] = useState<boolean>(true);
const [isExpanded, setIsExpanded] = useState(false);
const previewLength = 300;

const toggleDescription = () => {
setIsExpanded(!isExpanded);
}

const handleSubmit = async (e) => {
setSubmitted(await uploadSubmissionFile(e, project_id));
}

useEffect(() => {
const fetchProject = async () => {
try {
const project: Project = await getProject(+project_id);
setProjectData(project)
} catch (e) {
console.error(e)
}
}
fetchProject().then(() => setLoadingProject(false));
}, [project_id]);

function folderAdded(event: any) {
let newpaths: string[] = []
for (const file of event.target.files) {
let text: string = file.webkitRelativePath;
if (text.includes("/")) {
text = text.substring((text.indexOf("/") ?? 0) + 1, text.length);
}
newpaths.push(text);
}
setPaths(newpaths);
}

if (loadingProject) {
return <LinearProgress/>;
}

return (
<ThemeProvider theme={baseTheme}>
<Grid container alignItems="flex-start" justifyContent="flex-start"
style={{minHeight: '100vh', padding: 0}}>
<Grid item xs={12} style={{position: 'absolute', top: 84, left: 20}}>
<ProjectReturnButton locale={locale} project_id={projectData?.project_id}/>
</Grid>
<Grid item xs={12} style={{display: 'flex', justifyContent: 'center', paddingTop: 20}}>
<Card raised style={{width: 800}}>
<CardContent>
<Typography
variant="h3"
sx={{
fontWeight: 'medium'
}}
>
{projectData?.name}
</Typography>
<Divider style={{marginBottom: 10, marginTop: 10}}/>
<Typography>
{projectData?.description && projectData?.description.length > previewLength && !isExpanded
? `${projectData?.description.substring(0, previewLength)}...`
: projectData?.description
}
</Typography>
{projectData?.description && projectData?.description.length > previewLength && (
<IconButton
color="primary"
onClick={toggleDescription}
sx={{flex: '0 0 auto', padding: 0}}
>
{isExpanded ? <ExpandLessIcon/> : <ExpandMoreIcon/>}
</IconButton>
)}
<Typography
variant="h6"
sx={{
fontWeight: 'bold',
marginTop: 2
}}
>
{t('files')}
</Typography>

<Box component="form" onSubmit={handleSubmit} encType="multipart/form-data">
<Input
sx={{
border: '2px dashed',
borderColor: baseTheme.palette.primary.main,
borderRadius: 2,
textAlign: 'center',
marginTop: 1,
p: 4,
cursor: 'pointer',
'&:hover': {
backgroundColor: baseTheme.palette.background.default,
},
}}
onChange={folderAdded}
type="file"
id="filepicker"
name="fileList"
inputProps={{webkitdirectory: 'true', multiple: true}}
/>

<Tree paths={paths}/>

{submitted === 'yes' && (
<Box sx={{
display: 'flex',
alignItems: 'center',
color: baseTheme.palette.success.main,
mb: 1
}}>
<CheckCircleIcon sx={{mr: 1}}/>
<Typography variant="h6" sx={{fontWeight: 'bold', fontSize: '0.875rem'}}>
{t('submitted')}
</Typography>
</Box>
)}
{submitted === 'error' && (
<Box sx={{
display: 'flex',
alignItems: 'center',
color: baseTheme.palette.failure.main,
mb: 1
}}>
<ErrorIcon sx={{mr: 1}}/>
<Typography variant="h6" sx={{fontWeight: 'bold', fontSize: '0.875rem'}}>
{t('submission_error')}
</Typography>
</Box>
)}
{submitted !== 'yes' && (
<Button
variant="contained"
color="primary"
startIcon={<PublishIcon/>}
type="submit"
>
{t('submit')}
</Button>
)}
</Box>
</CardContent>
</Card>
</Grid>
</Grid>
</ThemeProvider>
);
}

export default SubmitDetailsPage;
68 changes: 68 additions & 0 deletions frontend/app/[locale]/components/Tree.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import React from 'react';
import {List} from '@mui/material';
import TreeNode from '@app/[locale]/components/TreeNode';

interface TreeNodeData {
name: string;
level?: number;
isLeaf?: boolean;
children?: TreeNodeData[];
}

interface TreeNodeDataMap {
[key: string]: TreeNodeData;
}

function createTreeStructure(paths: string[]): TreeNodeDataMap {
const tree: TreeNodeDataMap = {};

paths.forEach((path) => {
const parts = path.split('/');
let currentLevel = tree;

parts.forEach((part, index) => {
if (!currentLevel[part]) {
currentLevel[part] = {
name: part,
children: {},
} as TreeNodeData;
}
if (index === parts.length - 1) {
currentLevel[part].isLeaf = true;
} else {
currentLevel = currentLevel[part].children as TreeNodeDataMap;
}
});
});

return tree;
}

function convertToNodes(tree: TreeNodeDataMap, level: number = 0): TreeNodeData[] {
return Object.values(tree).map((node) => ({
name: node.name,
level: level,
isLeaf: node.isLeaf ?? false,
children: node.children ? convertToNodes(node.children as TreeNodeDataMap, level + 1) : [],
}));
}

// Tree component
interface TreeProps {
paths: string[];
}

const Tree: React.FC<TreeProps> = ({paths}) => {
const treeData = createTreeStructure(paths);
const nodes = convertToNodes(treeData);

return (
<List>
{nodes.map((node) => (
<TreeNode key={node.name} node={node}/>
))}
</List>
);
};

export default Tree;
48 changes: 48 additions & 0 deletions frontend/app/[locale]/components/TreeNode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import React, {useEffect, useState} from 'react';
import {Collapse, IconButton, List, ListItem, ListItemText} from '@mui/material';
import ExpandLess from '@mui/icons-material/ExpandLess';
import ExpandMore from '@mui/icons-material/ExpandMore';

interface TreeNodeProps {
node: {
name: string;
level: number;
isLeaf: boolean;
children: TreeNodeProps['node'][];
};
initiallyOpen?: boolean;
}

const TreeNode: React.FC<TreeNodeProps> = ({node, initiallyOpen = false}) => {
const [open, setOpen] = useState(initiallyOpen);

const handleClick = () => setOpen(!open);

useEffect(() => {
setOpen(initiallyOpen);
}, [initiallyOpen]);

return (
<>
<ListItem onClick={handleClick} dense sx={{pl: node.level * 2}}>
<ListItemText primary={node.name}/>
{!node.isLeaf && (
<IconButton edge="end">
{open ? <ExpandLess/> : <ExpandMore/>}
</IconButton>
)}
</ListItem>
{!node.isLeaf && (
<Collapse in={open} timeout="auto" unmountOnExit>
<List component="div" disablePadding>
{node.children.map((child) => (
<TreeNode key={child.name} node={child} initiallyOpen={false}/>
))}
</List>
</Collapse>
)}
</>
);
};

export default TreeNode;
Loading
Loading