arch: move web folder into api & move api to top level
This commit is contained in:
405
frontend/src/Projects/index.tsx
Normal file
405
frontend/src/Projects/index.tsx
Normal file
@ -0,0 +1,405 @@
|
||||
import React, { useState, useContext, useEffect } from 'react';
|
||||
import styled from 'styled-components/macro';
|
||||
import GlobalTopNavbar from 'App/TopNavbar';
|
||||
import Empty from 'shared/undraw/Empty';
|
||||
import {
|
||||
useCreateTeamMutation,
|
||||
useGetProjectsQuery,
|
||||
useCreateProjectMutation,
|
||||
GetProjectsDocument,
|
||||
GetProjectsQuery,
|
||||
} from 'shared/generated/graphql';
|
||||
|
||||
import ProjectGridItem, { AddProjectItem } from 'shared/components/ProjectGridItem';
|
||||
import { Link } from 'react-router-dom';
|
||||
import Navbar from 'App/Navbar';
|
||||
import NewProject from 'shared/components/NewProject';
|
||||
import UserIDContext from 'App/context';
|
||||
import Button from 'shared/components/Button';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import Input from 'shared/components/Input';
|
||||
import updateApolloCache from 'shared/utils/cache';
|
||||
import produce from 'immer';
|
||||
const EmptyStateContent = styled.div`
|
||||
display: flex;
|
||||
justy-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
`;
|
||||
|
||||
const EmptyStateTitle = styled.h3`
|
||||
color: #fff;
|
||||
font-size: 18px;
|
||||
`;
|
||||
|
||||
const EmptyStatePrompt = styled.span`
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
font-size: 16px;
|
||||
margin-top: 8px;
|
||||
`;
|
||||
const EmptyState = styled(Empty)`
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
`;
|
||||
const CreateTeamButton = styled(Button)`
|
||||
width: 100%;
|
||||
`;
|
||||
type CreateTeamData = { teamName: string };
|
||||
type CreateTeamFormProps = {
|
||||
onCreateTeam: (teamName: string) => void;
|
||||
};
|
||||
const CreateTeamFormContainer = styled.form``;
|
||||
|
||||
const CreateTeamForm: React.FC<CreateTeamFormProps> = ({ onCreateTeam }) => {
|
||||
const { register, handleSubmit, errors } = useForm<CreateTeamData>();
|
||||
const createTeam = (data: CreateTeamData) => {
|
||||
onCreateTeam(data.teamName);
|
||||
};
|
||||
return (
|
||||
<CreateTeamFormContainer onSubmit={handleSubmit(createTeam)}>
|
||||
<Input
|
||||
width="100%"
|
||||
label="Team name"
|
||||
id="teamName"
|
||||
name="teamName"
|
||||
variant="alternate"
|
||||
ref={register({ required: 'Team name is required' })}
|
||||
/>
|
||||
<CreateTeamButton type="submit">Create</CreateTeamButton>
|
||||
</CreateTeamFormContainer>
|
||||
);
|
||||
};
|
||||
|
||||
const ProjectAddTile = styled.div`
|
||||
background-color: rgba(${props => props.theme.colors.bg.primary}, 0.4);
|
||||
background-size: cover;
|
||||
background-position: 50%;
|
||||
color: #fff;
|
||||
line-height: 20px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const ProjectTile = styled(Link)<{ color: string }>`
|
||||
background-color: ${props => props.color};
|
||||
background-size: cover;
|
||||
background-position: 50%;
|
||||
color: #fff;
|
||||
line-height: 20px;
|
||||
padding: 8px;
|
||||
position: relative;
|
||||
text-decoration: none;
|
||||
|
||||
border-radius: 3px;
|
||||
display: block;
|
||||
`;
|
||||
|
||||
const ProjectTileFade = styled.div`
|
||||
background-color: rgba(0, 0, 0, 0.15);
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
`;
|
||||
|
||||
const ProjectListItem = styled.li`
|
||||
width: 23.5%;
|
||||
padding: 0;
|
||||
margin: 0 2% 2% 0;
|
||||
|
||||
box-sizing: border-box;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover ${ProjectTileFade} {
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
}
|
||||
`;
|
||||
|
||||
const ProjectList = styled.ul`
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
& ${ProjectListItem}:nth-of-type(4n) {
|
||||
margin-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const ProjectTileDetails = styled.div`
|
||||
display: flex;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
`;
|
||||
|
||||
const ProjectAddTileDetails = styled.div`
|
||||
display: flex;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ProjectTileName = styled.div<{ centered?: boolean }>`
|
||||
flex: 0 0 auto;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
max-height: 40px;
|
||||
width: 100%;
|
||||
word-wrap: break-word;
|
||||
${props => props.centered && 'text-align: center;'}
|
||||
`;
|
||||
|
||||
const Wrapper = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: flex-start;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
const ProjectSectionTitleWrapper = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
height: 32px;
|
||||
margin-bottom: 24px;
|
||||
padding: 8px 0;
|
||||
position: relative;
|
||||
margin-top: 16px;
|
||||
`;
|
||||
|
||||
const SectionActions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const SectionAction = styled(Button)`
|
||||
padding: 6px 12px;
|
||||
`;
|
||||
const SectionActionLink = styled(Link)`
|
||||
margin-right: 8px;
|
||||
`;
|
||||
|
||||
const ProjectSectionTitle = styled.h3`
|
||||
font-size: 16px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
||||
|
||||
const ProjectsContainer = styled.div`
|
||||
margin: 40px 16px 0;
|
||||
width: 100%;
|
||||
max-width: 825px;
|
||||
min-width: 288px;
|
||||
`;
|
||||
const ProjectGrid = styled.div`
|
||||
max-width: 780px;
|
||||
display: grid;
|
||||
grid-template-columns: 240px 240px 240px;
|
||||
gap: 20px 10px;
|
||||
`;
|
||||
const AddTeamButton = styled(Button)`
|
||||
padding: 6px 12px;
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 12px;
|
||||
`;
|
||||
|
||||
const CreateFirstTeam = styled(Button)`
|
||||
margin-top: 8px;
|
||||
`;
|
||||
type ShowNewProject = {
|
||||
open: boolean;
|
||||
initialTeamID: null | string;
|
||||
};
|
||||
|
||||
const ProjectLink = styled(Link)``;
|
||||
|
||||
const Projects = () => {
|
||||
const { showPopup, hidePopup } = usePopup();
|
||||
const { loading, data } = useGetProjectsQuery();
|
||||
useEffect(() => {
|
||||
document.title = 'Citadel';
|
||||
}, []);
|
||||
const [createProject] = useCreateProjectMutation({
|
||||
update: (client, newProject) => {
|
||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.projects.push({ ...newProject.data.createProject });
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const [showNewProject, setShowNewProject] = useState<ShowNewProject>({ open: false, initialTeamID: null });
|
||||
const { userID, setUserID } = useContext(UserIDContext);
|
||||
const [createTeam] = useCreateTeamMutation({
|
||||
update: (client, createData) => {
|
||||
updateApolloCache<GetProjectsQuery>(client, GetProjectsDocument, cache =>
|
||||
produce(cache, draftCache => {
|
||||
draftCache.teams.push({ ...createData.data.createTeam });
|
||||
}),
|
||||
);
|
||||
},
|
||||
});
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<span>loading</span>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
|
||||
if (data) {
|
||||
const { projects, teams, organizations } = data;
|
||||
const organizationID = organizations[0].id ?? null;
|
||||
const projectTeams = teams.map(team => {
|
||||
return {
|
||||
id: team.id,
|
||||
name: team.name,
|
||||
projects: projects.filter(project => project.team.id === team.id),
|
||||
};
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<GlobalTopNavbar onSaveProjectName={() => {}} projectID={null} name={null} />
|
||||
<Wrapper>
|
||||
<ProjectsContainer>
|
||||
<AddTeamButton
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup
|
||||
title="Create team"
|
||||
tab={0}
|
||||
onClose={() => {
|
||||
hidePopup();
|
||||
}}
|
||||
>
|
||||
<CreateTeamForm
|
||||
onCreateTeam={teamName => {
|
||||
if (organizationID) {
|
||||
createTeam({ variables: { name: teamName, organizationID } });
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Add Team
|
||||
</AddTeamButton>
|
||||
{projectTeams.length === 0 && (
|
||||
<EmptyStateContent>
|
||||
<EmptyState width={425} height={425} />
|
||||
<EmptyStateTitle>No teams exist</EmptyStateTitle>
|
||||
<EmptyStatePrompt>Create a new team to get started</EmptyStatePrompt>
|
||||
<CreateFirstTeam
|
||||
variant="outline"
|
||||
onClick={$target => {
|
||||
showPopup(
|
||||
$target,
|
||||
<Popup
|
||||
title="Create team"
|
||||
tab={0}
|
||||
onClose={() => {
|
||||
hidePopup();
|
||||
}}
|
||||
>
|
||||
<CreateTeamForm
|
||||
onCreateTeam={teamName => {
|
||||
if (organizationID) {
|
||||
createTeam({ variables: { name: teamName, organizationID } });
|
||||
hidePopup();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Popup>,
|
||||
);
|
||||
}}
|
||||
>
|
||||
Create new team
|
||||
</CreateFirstTeam>
|
||||
</EmptyStateContent>
|
||||
)}
|
||||
{projectTeams.map(team => {
|
||||
return (
|
||||
<div key={team.id}>
|
||||
<ProjectSectionTitleWrapper>
|
||||
<ProjectSectionTitle>{team.name}</ProjectSectionTitle>
|
||||
<SectionActions>
|
||||
<SectionActionLink to={`/teams/${team.id}`}>
|
||||
<SectionAction variant="outline">Projects</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/members`}>
|
||||
<SectionAction variant="outline">Members</SectionAction>
|
||||
</SectionActionLink>
|
||||
<SectionActionLink to={`/teams/${team.id}/settings`}>
|
||||
<SectionAction variant="outline">Settings</SectionAction>
|
||||
</SectionActionLink>
|
||||
</SectionActions>
|
||||
</ProjectSectionTitleWrapper>
|
||||
<ProjectList>
|
||||
{team.projects.map((project, idx) => (
|
||||
<ProjectListItem key={project.id}>
|
||||
<ProjectTile color={colors[idx % 5]} to={`/projects/${project.id}`}>
|
||||
<ProjectTileFade />
|
||||
<ProjectTileDetails>
|
||||
<ProjectTileName>{project.name}</ProjectTileName>
|
||||
</ProjectTileDetails>
|
||||
</ProjectTile>
|
||||
</ProjectListItem>
|
||||
))}
|
||||
<ProjectListItem>
|
||||
<ProjectAddTile
|
||||
onClick={() => {
|
||||
setShowNewProject({ open: true, initialTeamID: team.id });
|
||||
}}
|
||||
>
|
||||
<ProjectTileFade />
|
||||
<ProjectAddTileDetails>
|
||||
<ProjectTileName centered>Create new project</ProjectTileName>
|
||||
</ProjectAddTileDetails>
|
||||
</ProjectAddTile>
|
||||
</ProjectListItem>
|
||||
</ProjectList>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
{showNewProject.open && (
|
||||
<NewProject
|
||||
initialTeamID={showNewProject.initialTeamID}
|
||||
onCreateProject={(name, teamID) => {
|
||||
if (userID) {
|
||||
createProject({ variables: { teamID, name, userID } });
|
||||
setShowNewProject({ open: false, initialTeamID: null });
|
||||
}
|
||||
}}
|
||||
onClose={() => {
|
||||
setShowNewProject({ open: false, initialTeamID: null });
|
||||
}}
|
||||
teams={teams}
|
||||
/>
|
||||
)}
|
||||
</ProjectsContainer>
|
||||
</Wrapper>
|
||||
</>
|
||||
);
|
||||
}
|
||||
return <div>Error!</div>;
|
||||
};
|
||||
|
||||
export default Projects;
|
Reference in New Issue
Block a user