arch: move web folder into api & move api to top level

This commit is contained in:
Jordan Knott
2020-07-04 18:08:37 -05:00
parent eaffaa70df
commit e5d5e6da01
354 changed files with 20 additions and 1557 deletions

View File

@ -0,0 +1,129 @@
import { createGlobalStyle } from 'styled-components';
import { color, font, mixin } from 'shared/utils/styles';
export default createGlobalStyle`
html, body, #root {
height: 100%;
min-height: 100%;
min-width: 768px;
background: #262c49;
}
body {
color: ${color.textDarkest};
-webkit-tap-highlight-color: transparent;
line-height: 1.2;
${font.size(16)}
${font.regular}
}
#root {
display: flex;
flex-direction: column;
}
button,
input,
optgroup,
select,
textarea {
${font.regular}
}
*, *:after, *:before, input[type="search"] {
box-sizing: border-box;
}
a {
color: inherit;
text-decoration: none;
}
ul {
list-style: none;
}
ul, li, ol, dd, h1, h2, h3, h4, h5, h6, p {
padding: 0;
margin: 0;
}
h1, h2, h3, h4, h5, h6, strong {
${font.bold}
}
button {
background: none;
border: none;
}
/* Workaround for IE11 focus highlighting for select elements */
select::-ms-value {
background: none;
color: #42413d;
}
[role="button"], button, input, select, textarea {
outline: none;
&:focus {
outline: none;
}
&:disabled {
opacity: 0.5;
}
}
[role="button"], button, input, textarea {
appearance: none;
}
select:-moz-focusring {
color: transparent;
text-shadow: 0 0 0 #000;
}
select::-ms-expand {
display: none;
}
select option {
color: ${color.textDarkest};
}
p {
line-height: 1.4285;
a {
${mixin.link()}
}
}
textarea {
line-height: 1.4285;
}
body, select {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html {
touch-action: manipulation;
}
textarea {
resize: none;
}
::-webkit-scrollbar {
width: 10px;
}
::-webkit-scrollbar-track {
background: #262c49;
border-radius: 20px;
}
::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 20px;
}
${mixin.placeholderColor(color.textLight)}
`;

View File

@ -0,0 +1,31 @@
import React, { useContext } from 'react';
import { Home, Stack } from 'shared/icons';
import Navbar, { ActionButton, ButtonContainer, PrimaryLogo } from 'shared/components/Navbar';
import { Link } from 'react-router-dom';
import UserIDContext from './context';
const GlobalNavbar = () => {
const { userID } = useContext(UserIDContext);
if (!userID) {
return null;
}
return (
<Navbar>
<PrimaryLogo />
<ButtonContainer>
<Link to="/">
<ActionButton name="Home">
<Home width={28} height={28} />
</ActionButton>
</Link>
<Link to="/projects">
<ActionButton name="Projects">
<Stack size={28} color="#c2c6dc" />
</ActionButton>
</Link>
</ButtonContainer>
</Navbar>
);
};
export default GlobalNavbar;

View File

@ -0,0 +1,152 @@
import { createGlobalStyle } from 'styled-components';
/** DO NOT ALTER THIS FILE. It is a copy of https://necolas.github.io/normalize.css/ */
export default createGlobalStyle`
html {
line-height: 1.15;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
}
main {
display: block;
}
h1 {
font-size: 2em;
margin: 0.67em 0;
}
hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
pre {
font-family: monospace, monospace;
font-size: 1em;
}
a {
background-color: transparent;
}
abbr[title] {
border-bottom: none;
text-decoration: underline;
text-decoration: underline dotted;
}
b,
strong {
font-weight: bolder;
}
code,
kbd,
samp {
font-family: monospace, monospace;
font-size: 1em;
}
small {
font-size: 80%;
}
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
img {
border-style: none;
}
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
font-size: 100%;
line-height: 1.15;
margin: 0;
}
button,
input {
overflow: visible;
}
button,
select {
text-transform: none;
}
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
fieldset {
padding: 0.35em 0.75em 0.625em;
}
legend {
box-sizing: border-box;
color: inherit;
display: table;
max-width: 100%;
padding: 0;
white-space: normal;
}
progress {
vertical-align: baseline;
}
textarea {
overflow: auto;
}
[type="checkbox"],
[type="radio"] {
box-sizing: border-box;
padding: 0;
}
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
[type="search"] {
-webkit-appearance: textfield;
outline-offset: -2px;
}
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-file-upload-button {
-webkit-appearance: button;
font: inherit;
}
details {
display: block;
}
summary {
display: list-item;
}
template {
display: none;
}
[hidden] {
display: none;
}
`;

View File

@ -0,0 +1,40 @@
import React from 'react';
import { Router, Switch, Route } from 'react-router-dom';
import * as H from 'history';
import Dashboard from 'Dashboard';
import Admin from 'Admin';
import Projects from 'Projects';
import Project from 'Projects/Project';
import Teams from 'Teams';
import Login from 'Auth';
import Profile from 'Profile';
import styled from 'styled-components';
const MainContent = styled.div`
padding: 0 0 0 0;
background: #262c49;
height: 100%;
display: flex;
flex-direction: column;
flex-grow: 1;
`;
type RoutesProps = {
history: H.History;
};
const Routes = ({ history }: RoutesProps) => (
<Switch>
<Route exact path="/login" component={Login} />
<MainContent>
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectID" component={Project} />
<Route path="/teams/:teamID" component={Teams} />
<Route path="/profile" component={Profile} />
<Route path="/admin" component={Admin} />
</MainContent>
</Switch>
);
export default Routes;

View File

@ -0,0 +1,28 @@
import { createGlobalStyle, DefaultTheme } from 'styled-components';
const theme: DefaultTheme = {
borderRadius: {
primary: '3px',
alternate: '6px',
},
colors: {
primary: '115, 103, 240',
secondary: '216, 93, 216',
alternate: '65, 69, 97',
success: '40, 199, 111',
danger: '234, 84, 85',
warning: '255, 159, 67',
dark: '30, 30, 30',
text: {
primary: '194, 198, 220',
secondary: '255, 255, 255',
},
border: '65, 69, 97',
bg: {
primary: '16, 22, 58',
secondary: '38, 44, 73',
},
},
};
export { theme };

View File

@ -0,0 +1,333 @@
import React, { useState, useContext, useEffect } from 'react';
import TopNavbar, { MenuItem } from 'shared/components/TopNavbar';
import styled from 'styled-components/macro';
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings, { DeleteConfirm, DELETE_INFO } from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
import {
RoleCode,
useMeQuery,
useDeleteProjectMutation,
useGetProjectsQuery,
GetProjectsDocument,
} from 'shared/generated/graphql';
import { usePopup, Popup } from 'shared/components/PopupMenu';
import { History } from 'history';
import produce from 'immer';
import { Link } from 'react-router-dom';
const TeamContainer = styled.div`
display: flex;
flex-direction: column;
`;
const TeamTitle = styled.h3`
font-size: 14px;
font-weight: 700;
`;
const TeamProjects = styled.div`
display: flex;
flex-direction: column;
margin-top: 8px;
margin-bottom: 4px;
`;
const TeamProjectLink = styled(Link)`
display: flex;
font-weight: 700;
height: 36px;
overflow: hidden;
padding: 0;
position: relative;
text-decoration: none;
user-select: none;
`;
const TeamProjectBackground = styled.div<{ color: string }>`
background-image: url(null);
background-color: ${props => props.color};
background-size: cover;
background-position: 50%;
position: absolute;
width: 100%;
height: 36px;
opacity: 1;
border-radius: 3px;
&:before {
background: rgba(${props => props.theme.colors.bg.secondary});
bottom: 0;
content: '';
left: 0;
opacity: 0.88;
position: absolute;
right: 0;
top: 0;
}
`;
const TeamProjectAvatar = styled.div<{ color: string }>`
background-image: url(null);
background-color: ${props => props.color};
display: inline-block;
flex: 0 0 auto;
background-size: cover;
border-radius: 3px 0 0 3px;
height: 36px;
width: 36px;
position: relative;
opacity: 0.7;
`;
const TeamProjectContent = styled.div`
display: flex;
position: relative;
flex: 1;
width: 100%;
padding: 9px 0 9px 10px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const TeamProjectTitle = styled.div`
font-weight: 700;
display: block;
padding-right: 12px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const TeamProjectContainer = styled.div`
box-sizing: border-box;
border-radius: 3px;
position: relative;
margin: 0 4px 4px 0;
min-width: 0;
&:hover ${TeamProjectTitle} {
color: rgba(${props => props.theme.colors.text.secondary});
}
&:hover ${TeamProjectAvatar} {
opacity: 1;
}
&:hover ${TeamProjectBackground}:before {
opacity: 0.78;
}
`;
const colors = ['#e362e3', '#7a6ff0', '#37c5ab', '#aa62e3', '#e8384f'];
const ProjectFinder = () => {
const { loading, data } = useGetProjectsQuery();
if (loading) {
return <span>loading</span>;
}
if (data) {
const { projects, teams, organizations } = data;
const projectTeams = teams.map(team => {
return {
id: team.id,
name: team.name,
projects: projects.filter(project => project.team.id === team.id),
};
});
return (
<>
{projectTeams.map(team => (
<TeamContainer key={team.id}>
<TeamTitle>{team.name}</TeamTitle>
<TeamProjects>
{team.projects.map((project, idx) => (
<TeamProjectContainer key={project.id}>
<TeamProjectLink to={`/projects/${project.id}`}>
<TeamProjectBackground color={colors[idx % 5]} />
<TeamProjectAvatar color={colors[idx % 5]} />
<TeamProjectContent>
<TeamProjectTitle>{project.name}</TeamProjectTitle>
</TeamProjectContent>
</TeamProjectLink>
</TeamProjectContainer>
))}
</TeamProjects>
</TeamContainer>
))}
</>
);
}
return <span>error</span>;
};
type ProjectPopupProps = {
history: History<History.PoorMansUnknown>;
name: string;
projectID: string;
};
export const ProjectPopup: React.FC<ProjectPopupProps> = ({ history, name, projectID }) => {
const { hidePopup, setTab } = usePopup();
const [deleteProject] = useDeleteProjectMutation({
update: (client, deleteData) => {
const cacheData: any = client.readQuery({
query: GetProjectsDocument,
});
console.log(cacheData);
console.log(deleteData);
const newData = produce(cacheData, (draftState: any) => {
draftState.projects = draftState.projects.filter(
(project: any) => project.id !== deleteData.data.deleteProject.project.id,
);
});
client.writeQuery({
query: GetProjectsDocument,
data: {
...newData,
},
});
},
});
return (
<>
<Popup title={null} tab={0}>
<ProjectSettings
onDeleteProject={() => {
setTab(1);
}}
/>
</Popup>
<Popup title={`Delete the "${name}" project?`} tab={1}>
<DeleteConfirm
description={DELETE_INFO.DELETE_PROJECTS.description}
deletedItems={DELETE_INFO.DELETE_PROJECTS.deletedItems}
onConfirmDelete={() => {
if (projectID) {
deleteProject({ variables: { projectID } });
hidePopup();
history.push('/projects');
}
}}
/>
</Popup>
</>
);
};
type GlobalTopNavbarProps = {
nameOnly?: boolean;
projectID: string | null;
onChangeProjectOwner?: (userID: string) => void;
name: string | null;
currentTab?: number;
popupContent?: JSX.Element;
menuType?: Array<MenuItem>;
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
projectMembers?: null | Array<TaskUser>;
onSaveProjectName?: (projectName: string) => void;
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
onSetTab?: (tab: number) => void;
onRemoveFromBoard?: (userID: string) => void;
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({
currentTab,
onSetTab,
menuType,
projectID,
onChangeProjectOwner,
onChangeRole,
name,
popupContent,
projectMembers,
onInviteUser,
onSaveProjectName,
onRemoveFromBoard,
nameOnly,
}) => {
console.log(popupContent);
const { loading, data } = useMeQuery();
const { showPopup, hidePopup, setTab } = usePopup();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const onLogout = () => {
fetch('http://localhost:3333/auth/logout', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
if (status === 200) {
history.replace('/login');
setUserID(null);
hidePopup();
}
});
};
const onProfileClick = ($target: React.RefObject<HTMLElement>) => {
showPopup(
$target,
<Popup title={null} tab={0}>
<ProfileMenu
onLogout={onLogout}
onAdminConsole={() => {
history.push('/admin');
hidePopup();
}}
onProfile={() => {
history.push('/profile');
hidePopup();
}}
/>
</Popup>,
195,
);
};
const onOpenSettings = ($target: React.RefObject<HTMLElement>) => {
console.log('maybe firing popup');
if (popupContent) {
console.log('showing popup');
showPopup($target, popupContent, 185);
}
};
if (!userID) {
return null;
}
return (
<>
<TopNavbar
name={name}
menuType={menuType}
onOpenProjectFinder={$target => {
showPopup(
$target,
<Popup tab={0} title={null}>
<ProjectFinder />
</Popup>,
);
}}
currentTab={currentTab}
user={data ? data.me : null}
onInviteUser={onInviteUser}
onChangeRole={onChangeRole}
onChangeProjectOwner={onChangeProjectOwner}
onNotificationClick={() => {}}
onSetTab={onSetTab}
onRemoveFromBoard={onRemoveFromBoard}
onDashboardClick={() => {
history.push('/');
}}
projectMembers={projectMembers}
onProfileClick={onProfileClick}
onSaveName={onSaveProjectName}
onOpenSettings={onOpenSettings}
/>
</>
);
};
export default GlobalTopNavbar;

View File

@ -0,0 +1,9 @@
import React from 'react';
type UserIDContextState = {
userID: string | null;
setUserID: (userID: string | null) => void;
};
export const UserIDContext = React.createContext<UserIDContextState>({ userID: null, setUserID: _userID => null });
export default UserIDContext;

View File

@ -0,0 +1,63 @@
import React, { useState, useEffect } from 'react';
import jwtDecode from 'jwt-decode';
import { createBrowserHistory } from 'history';
import { setAccessToken } from 'shared/utils/accessToken';
import styled, { ThemeProvider } from 'styled-components';
import NormalizeStyles from './NormalizeStyles';
import BaseStyles from './BaseStyles';
import { theme } from './ThemeStyles';
import Routes from './Routes';
import { UserIDContext } from './context';
import Navbar from './Navbar';
import { Router } from 'react-router';
import { PopupProvider } from 'shared/components/PopupMenu';
const history = createBrowserHistory();
const App = () => {
const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
useEffect(() => {
fetch('http://localhost:3333/auth/refresh_token', {
method: 'POST',
credentials: 'include',
}).then(async x => {
const { status } = x;
if (status === 400) {
history.replace('/login');
} else {
const response: RefreshTokenResponse = await x.json();
const { accessToken } = response;
const claims: JWTToken = jwtDecode(accessToken);
setUserID(claims.userId);
setAccessToken(accessToken);
}
setLoading(false);
});
}, []);
return (
<>
<UserIDContext.Provider value={{ userID, setUserID }}>
<ThemeProvider theme={theme}>
<NormalizeStyles />
<BaseStyles />
<Router history={history}>
<PopupProvider>
{loading ? (
<div>loading</div>
) : (
<>
<Routes history={history} />
</>
)}
</PopupProvider>
</Router>
</ThemeProvider>
</UserIDContext.Provider>
</>
);
};
export default App;