Skip to content

Commit

Permalink
Merge pull request #153 from SELab-2/140-project-submit-page
Browse files Browse the repository at this point in the history
140 project submit page
  • Loading branch information
gilles-arnout authored May 12, 2024
2 parents 89d9755 + 56f8d70 commit a6ede4e
Show file tree
Hide file tree
Showing 9 changed files with 822 additions and 123 deletions.
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

0 comments on commit a6ede4e

Please sign in to comment.