feature: various additions

This commit is contained in:
Jordan Knott
2020-06-12 17:21:58 -05:00
parent 4c02df9061
commit 6267a37b6e
72 changed files with 2038 additions and 389 deletions

View File

@ -7,3 +7,7 @@ indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[Makefile]
indent_style = tab
indent_size = 2

3
web/Makefile Normal file
View File

@ -0,0 +1,3 @@
start:
yarn start

View File

@ -18,7 +18,9 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.3.2",
"@testing-library/user-event": "^7.1.2",
"@types/axios": "^0.14.0",
"@types/color": "^3.0.1",
"@types/date-fns": "^2.6.0",
"@types/jest": "^24.0.0",
"@types/jwt-decode": "^2.2.1",
"@types/lodash": "^4.14.149",
@ -32,6 +34,8 @@
"@types/react-select": "^3.0.13",
"@types/styled-components": "^5.0.0",
"@welldone-software/why-did-you-render": "^4.2.2",
"ag-grid-community": "^23.2.0",
"ag-grid-react": "^23.2.0",
"apollo-cache-inmemory": "^1.6.5",
"apollo-client": "^2.6.8",
"apollo-link": "^1.2.13",
@ -39,7 +43,10 @@
"apollo-link-http": "^1.5.16",
"apollo-link-state": "^0.4.2",
"apollo-utilities": "^1.3.3",
"axios": "^0.19.2",
"axios-auth-refresh": "^2.2.7",
"color": "^3.1.2",
"date-fns": "^2.14.0",
"graphql": "^15.0.0",
"graphql-tag": "^2.10.3",
"history": "^4.10.1",

View File

@ -6,7 +6,14 @@ import Dashboard from 'Dashboard';
import Projects from 'Projects';
import Project from 'Projects/Project';
import Login from 'Auth';
import Profile from 'Profile';
import styled from 'styled-components';
const MainContent = styled.div`
padding: 0 0 50px 80px;
background: #262c49;
height: 100%;
`;
type RoutesProps = {
history: H.History;
};
@ -14,9 +21,12 @@ type RoutesProps = {
const Routes = ({ history }: RoutesProps) => (
<Switch>
<Route exact path="/login" component={Login} />
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectID" component={Project} />
<MainContent>
<Route exact path="/" component={Dashboard} />
<Route exact path="/projects" component={Projects} />
<Route path="/projects/:projectID" component={Project} />
<Route path="/profile" component={Profile} />
</MainContent>
</Switch>
);

View File

@ -1,6 +1,6 @@
import React, { useState, useContext } from 'react';
import TopNavbar from 'shared/components/TopNavbar';
import DropdownMenu from 'shared/components/DropdownMenu';
import DropdownMenu, { ProfileMenu } from 'shared/components/DropdownMenu';
import ProjectSettings from 'shared/components/ProjectSettings';
import { useHistory } from 'react-router';
import UserIDContext from 'App/context';
@ -14,21 +14,37 @@ type GlobalTopNavbarProps = {
};
const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name, projectMembers, onSaveProjectName }) => {
const { loading, data } = useMeQuery();
const { showPopup } = usePopup();
const { showPopup, hidePopup } = usePopup();
const history = useHistory();
const { userID, setUserID } = useContext(UserIDContext);
const [menu, setMenu] = useState({
top: 0,
left: 0,
isOpen: false,
});
const onProfileClick = (bottom: number, right: number) => {
setMenu({
isOpen: !menu.isOpen,
left: right,
top: bottom,
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}
onProfile={() => {
history.push('/profile');
hidePopup();
}}
/>
</Popup>,
185,
);
};
const onOpenSettings = ($target: React.RefObject<HTMLElement>) => {
showPopup(
@ -40,18 +56,6 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name, projectMembers,
);
};
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);
}
});
};
if (!userID) {
return null;
}
@ -59,30 +63,13 @@ const GlobalTopNavbar: React.FC<GlobalTopNavbarProps> = ({ name, projectMembers,
<>
<TopNavbar
projectName={name}
bgColor={data ? data.me.profileIcon.bgColor ?? '#7367F0' : '#7367F0'}
firstName={data ? data.me.firstName : ''}
lastName={data ? data.me.lastName : ''}
initials={!data ? '' : data.me.profileIcon.initials ?? ''}
user={data ? data.me : null}
onNotificationClick={() => {}}
projectMembers={projectMembers}
onProfileClick={onProfileClick}
onSaveProjectName={onSaveProjectName}
onOpenSettings={onOpenSettings}
/>
{menu.isOpen && (
<DropdownMenu
onCloseDropdown={() => {
setMenu({
top: 0,
left: 0,
isOpen: false,
});
}}
onLogout={onLogout}
left={menu.left}
top={menu.top}
/>
)}
</>
);
};

View File

@ -13,12 +13,6 @@ import { PopupProvider } from 'shared/components/PopupMenu';
const history = createBrowserHistory();
const MainContent = styled.div`
padding: 0 0 50px 80px;
background: #262c49;
height: 100%;
`;
const App = () => {
const [loading, setLoading] = useState(true);
const [userID, setUserID] = useState<string | null>(null);
@ -54,9 +48,7 @@ const App = () => {
) : (
<>
<Navbar />
<MainContent>
<Routes history={history} />
</MainContent>
<Routes history={history} />
</>
)}
</Router>

71
web/src/Profile/index.tsx Normal file
View File

@ -0,0 +1,71 @@
import React, { useRef, useEffect } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { Link } from 'react-router-dom';
import { getAccessToken } from 'shared/utils/accessToken';
import Navbar from 'App/Navbar';
import Settings from 'shared/components/Settings';
import UserIDContext from 'App/context';
import { useMeQuery, useClearProfileAvatarMutation } from 'shared/generated/graphql';
import axios from 'axios';
const MainContent = styled.div`
padding: 0 0 50px 80px;
height: 100%;
background: #262c49;
`;
const Projects = () => {
const $fileUpload = useRef<HTMLInputElement>(null);
const [clearProfileAvatar] = useClearProfileAvatarMutation();
const { loading, data, refetch } = useMeQuery();
useEffect(() => {
document.title = 'Profile | Citadel';
}, []);
return (
<>
<input
type="file"
name="file"
style={{ display: 'none' }}
ref={$fileUpload}
onChange={e => {
if (e.target.files) {
console.log(e.target.files[0]);
const fileData = new FormData();
fileData.append('file', e.target.files[0]);
const accessToken = getAccessToken();
axios
.post('http://localhost:3333/users/me/avatar', fileData, {
headers: {
Authorization: `Bearer ${accessToken}`,
},
})
.then(res => {
if ($fileUpload && $fileUpload.current) {
$fileUpload.current.value = '';
refetch();
}
});
}
}}
/>
<GlobalTopNavbar onSaveProjectName={() => {}} name={null} />
{!loading && data && (
<Settings
profile={data.me.profileIcon}
onProfileAvatarChange={() => {
if ($fileUpload && $fileUpload.current) {
$fileUpload.current.click();
}
}}
onProfileAvatarRemove={() => {
clearProfileAvatar();
}}
/>
)}
</>
);
};
export default Projects;

View File

@ -1,4 +1,4 @@
import React, { useState, useRef, useContext } from 'react';
import React, { useState, useRef, useContext, useEffect } from 'react';
import GlobalTopNavbar from 'App/TopNavbar';
import styled from 'styled-components/macro';
import { Bolt, ToggleOn, Tags } from 'shared/icons';
@ -428,6 +428,11 @@ const Project = () => {
const $labelsRef = useRef<HTMLDivElement>(null);
const labelsRef = useRef<Array<ProjectLabel>>([]);
const taskLabelsRef = useRef<Array<TaskLabel>>([]);
useEffect(() => {
if (data) {
document.title = `${data.findProject.name} | Citadel`;
}
}, [data]);
if (loading) {
return (
<>
@ -562,6 +567,7 @@ const Project = () => {
</Popup>,
);
}}
onChangeTaskGroupName={(taskGroupID, name) => {}}
onQuickEditorOpen={onQuickEditorOpen}
onExtraMenuOpen={(taskGroupID: string, $targetRef: any) => {
showPopup(

View File

@ -1,4 +1,4 @@
import React, { useState, useContext } from 'react';
import React, { useState, useContext, useEffect } from 'react';
import styled from 'styled-components/macro';
import GlobalTopNavbar from 'App/TopNavbar';
import { useGetProjectsQuery, useCreateProjectMutation, GetProjectsDocument } from 'shared/generated/graphql';
@ -28,6 +28,9 @@ const ProjectLink = styled(Link)``;
const Projects = () => {
const { loading, data } = useGetProjectsQuery();
useEffect(() => {
document.title = 'Citadel';
}, []);
const [createProject] = useCreateProjectMutation({
update: (client, newProject) => {
const cacheData: any = client.readQuery({

View File

@ -18,8 +18,7 @@ type ContextMenuEvent = {
type TaskUser = {
id: string;
firstName: string;
lastName: string;
fullName: string;
profileIcon: ProfileIcon;
};
@ -32,6 +31,11 @@ type LoginFormData = {
password: string;
};
type DueDateFormData = {
endDate: Date;
endTime: string | null;
};
type LoginProps = {
onSubmit: (
data: LoginFormData,

View File

@ -9,9 +9,21 @@ import { onError } from 'apollo-link-error';
import { ApolloLink, Observable, fromPromise } from 'apollo-link';
import { getAccessToken, getNewToken, setAccessToken } from 'shared/utils/accessToken';
import axios from 'axios';
import createAuthRefreshInterceptor from 'axios-auth-refresh';
import App from './App';
// Function that will be called to refresh authorization
const refreshAuthLogic = (failedRequest: any) =>
axios.post('http://localhost:3333/auth/refresh_token', {}, { withCredentials: true }).then(tokenRefreshResponse => {
setAccessToken(tokenRefreshResponse.data.accessToken);
failedRequest.response.config.headers.Authorization = `Bearer ${tokenRefreshResponse.data.accessToken}`;
return Promise.resolve();
});
createAuthRefreshInterceptor(axios, refreshAuthLogic);
// https://able.bio/AnasT/apollo-graphql-async-access-token-refresh--470t1c8
let forward$;

View File

@ -0,0 +1,25 @@
import React, { useRef } from 'react';
import Admin from '.';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
export default {
component: Admin,
title: 'Admin',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Admin />
</>
);
};

View File

@ -0,0 +1,323 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User, Plus } from 'shared/icons';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/dist/styles/ag-grid.css';
import 'ag-grid-community/dist/styles/ag-theme-material.css';
const NewUserButton = styled.button`
outline: none;
border: none;
cursor: pointer;
line-height: 20px;
padding: 0.75rem;
background-color: transparent;
display: flex;
align-items: center;
justify-content: center;
color: rgba(115, 103, 240);
font-size: 14px;
border-radius: 0.5rem;
border-width: 1px;
border-style: solid;
border-color: transparent;
border-image: initial;
border-color: rgba(115, 103, 240);
span {
padding-left: 0.5rem;
}
`;
const GridTable = styled.div`
height: 620px;
`;
const RootWrapper = styled.div`
height: 100%;
display: flex;
position: relative;
flex-direction: column;
overflow: hidden;
`;
const Root = styled.div`
.ag-theme-material {
--ag-foreground-color: #c2c6dc;
--ag-secondary-foreground-color: #c2c6dc;
--ag-background-color: transparent;
--ag-header-background-color: transparent;
--ag-header-foreground-color: #c2c6dc;
--ag-border-color: #414561;
--ag-row-hover-color: #262c49;
--ag-header-cell-hover-background-color: #262c49;
--ag-checkbox-unchecked-color: #c2c6dc;
--ag-checkbox-indeterminate-color: rgba(115, 103, 240);
--ag-selected-row-background-color: #262c49;
--ag-material-primary-color: rgba(115, 103, 240);
--ag-material-accent-color: rgba(115, 103, 240);
}
.ag-theme-material ::-webkit-scrollbar {
width: 12px;
}
.ag-theme-material ::-webkit-scrollbar-track {
background: #262c49;
border-radius: 20px;
}
.ag-theme-material ::-webkit-scrollbar-thumb {
background: #7367f0;
border-radius: 20px;
}
.ag-header-cell-text {
color: #fff;
font-weight: 700;
}
`;
const Header = styled.div`
border-bottom: 1px solid #e2e2e2;
flex-direction: row;
box-sizing: border-box;
display: flex;
white-space: nowrap;
width: 100%;
overflow: hidden;
background: transparent;
border-bottom-color: #414561;
color: #fff;
height: 112px;
min-height: 112px;
`;
const ActionButtons = () => {
return <span>Hello!</span>;
};
const data = {
defaultColDef: {
resizable: true,
sortable: true,
},
columnDefs: [
{
minWidth: 125,
width: 125,
headerCheckboxSelection: true,
checkboxSelection: true,
headerName: 'ID',
field: 'id',
},
{ minWidth: 210, headerName: 'Username', editable: true, field: 'username' },
{ minWidth: 225, headerName: 'Email', field: 'email' },
{ minWidth: 200, headerName: 'Name', editable: true, field: 'full_name' },
{ minWidth: 200, headerName: 'Role', editable: true, field: 'role' },
{
minWidth: 200,
headerName: 'Actions',
cellRenderer: 'actionButtons',
},
],
frameworkComponents: {
actionButtons: ActionButtons,
},
rowData: [
{ id: '1', full_name: 'Jordan Knott', username: 'jordan', email: 'jordan@jordanthedev.com', role: 'Admin' },
{ id: '2', full_name: 'Jordan Test', username: 'jordantest', email: 'jordan@jordanthedev.com', role: 'Admin' },
{ id: '3', full_name: 'Jordan Other', username: 'alphatest1050', email: 'jordan@jordanthedev.com', role: 'Admin' },
{ id: '5', full_name: 'Jordan French', username: 'other', email: 'jordan@jordanthedev.com', role: 'Admin' },
],
};
const ListTable = () => {
return (
<Root>
<div className="ag-theme-material" style={{ height: '296px', width: '100%' }}>
<AgGridReact
rowSelection="multiple"
defaultColDef={data.defaultColDef}
columnDefs={data.columnDefs}
rowData={data.rowData}
frameworkComponents={data.frameworkComponents}
onFirstDataRendered={params => {
params.api.sizeColumnsToFit();
}}
onGridSizeChanged={params => {
params.api.sizeColumnsToFit();
}}
></AgGridReact>
</div>
</Root>
);
};
const Wrapper = styled.div`
background: #eff2f7;
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
`;
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
position: relative;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover {
color: rgba(115, 103, 240);
}
&:hover svg {
fill: rgba(115, 103, 240);
}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
position: relative;
display: block;
overflow: hidden;
width: 100%;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
margin-left: 1rem !important;
border-radius: 0.5rem;
`;
const items = [
{ name: 'Insights' },
{ name: 'Members' },
{ name: 'Teams' },
{ name: 'Security' },
{ name: 'Settings' },
];
type NavItemProps = {
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
};
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
};
const Admin = () => {
const [currentTop, setTop] = useState(0);
const [currentTab, setTab] = useState(0);
const $tabNav = useRef<HTMLDivElement>(null);
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<NewUserButton>
<Plus color="rgba(115, 103, 240)" size={10} />
<span>Add New</span>
</NewUserButton>
<ListTable />
</TabContent>
</TabContentWrapper>
</Container>
);
};
export default Admin;

View File

@ -21,6 +21,7 @@ import {
CardTitle,
CardMembers,
} from './Styles';
import TaskAssignee from 'shared/components/TaskAssignee';
type DueDate = {
isPastDue: boolean;
@ -143,7 +144,16 @@ const Card = React.forwardRef(
<CardMembers>
{members &&
members.map(member => (
<Member key={member.id} taskID={taskID} member={member} onCardMemberClick={onCardMemberClick} />
<TaskAssignee
key={member.id}
size={28}
member={member}
onMemberProfile={$target => {
if (onCardMemberClick) {
onCardMemberClick($target, taskID, member.id);
}
}}
/>
))}
</CardMembers>
</ListCardDetails>

View File

@ -33,4 +33,29 @@ const DropdownMenu: React.FC<DropdownMenuProps> = ({ left, top, onLogout, onClos
);
};
type ProfileMenuProps = {
onProfile: () => void;
onLogout: () => void;
};
const ProfileMenu: React.FC<ProfileMenuProps> = ({ onProfile, onLogout }) => {
return (
<>
<ActionItem onClick={onProfile}>
<User size={16} color="#c2c6dc" />
<ActionTitle>Profile</ActionTitle>
</ActionItem>
<Separator />
<ActionsList>
<ActionItem onClick={onLogout}>
<Exit size={16} color="#c2c6dc" />
<ActionTitle>Logout</ActionTitle>
</ActionItem>
</ActionsList>
</>
);
};
export { ProfileMenu };
export default DropdownMenu;

View File

@ -1,7 +1,12 @@
import React, { useRef } from 'react';
import { action } from '@storybook/addon-actions';
import DueDateManager from '.';
import { Popup } from '../PopupMenu';
import styled from 'styled-components';
const PopupWrapper = styled.div`
width: 300px;
`;
export default {
component: DueDateManager,
title: 'DueDateManager',
@ -15,41 +20,44 @@ export default {
export const Default = () => {
return (
<DueDateManager
task={{
id: '1',
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
<PopupWrapper>
<Popup title={null} tab={0}>
<DueDateManager
task={{
id: '1',
profileIcon: { url: null, initials: null, bgColor: null },
firstName: 'Jordan',
lastName: 'Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
taskGroup: { name: 'General', id: '1', position: 1 },
name: 'Hello, world',
position: 1,
labels: [
{
id: 'soft-skills',
assignedDate: new Date().toString(),
projectLabel: {
createdDate: new Date().toString(),
id: 'label-soft-skills',
name: 'Soft Skills',
labelColor: {
id: '1',
name: 'white',
colorHex: '#fff',
position: 1,
},
},
},
],
description: 'hello!',
assigned: [
{
id: '1',
profileIcon: { url: null, initials: null, bgColor: null },
fullName: 'Jordan Knott',
},
],
}}
onCancel={action('cancel')}
onDueDateChange={action('due date change')}
/>
</Popup>
</PopupWrapper>
);
};

View File

@ -1,8 +1,62 @@
import styled from 'styled-components';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div`
display: flex
flex-direction: column;
& .react-datepicker {
background: #262c49;
font-family: 'Droid Sans', sans-serif;
border: none;
}
& .react-datepicker__day-name {
color: #c2c6dc;
outline: none;
box-shadow: none;
padding: 4px;
font-size: 12px40px
line-height: 40px;
}
& .react-datepicker__day-name:hover {
background: #10163a;
}
& .react-datepicker__month {
margin: 0;
}
& .react-datepicker__day,
& .react-datepicker__time-name {
color: #c2c6dc;
outline: none;
box-shadow: none;
padding: 4px;
font-size: 14px;
}
& .react-datepicker__day--outside-month {
opacity: 0.6;
}
& .react-datepicker__day:hover {
border-radius: 50%;
background: #10163a;
}
& .react-datepicker__day--selected {
border-radius: 50%;
background: rgba(115, 103, 240);
color: #fff;
}
& .react-datepicker__day--selected:hover {
border-radius: 50%;
background: rgba(115, 103, 240);
color: #fff;
}
& .react-datepicker__header {
background: none;
border: none;
}
`;
export const DueDatePickerWrapper = styled.div`

View File

@ -1,23 +1,202 @@
import React, { useState } from 'react';
import React, { useState, useEffect } from 'react';
import moment from 'moment';
import styled from 'styled-components';
import DatePicker from 'react-datepicker';
import { Cross } from 'shared/icons';
import _ from 'lodash';
import { Wrapper, ActionWrapper, DueDatePickerWrapper, ConfirmAddDueDate, CancelDueDate } from './Styles';
import 'react-datepicker/dist/react-datepicker.css';
import { getYear, getMonth } from 'date-fns';
import { useForm } from 'react-hook-form';
type DueDateManagerProps = {
task: Task;
onDueDateChange: (task: Task, newDueDate: Date) => void;
onCancel: () => void;
};
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const [startDate, setStartDate] = useState(new Date());
const HeaderSelectLabel = styled.div`
display: inline-block;
position: relative;
z-index: 9999;
border-radius: 3px;
cursor: pointer;
padding: 6px 10px;
text-decoration: underline;
margin: 6px 0;
font-size: 14px;
line-height: 16px;
margin-left: 0;
margin-right: 0;
padding-left: 4px;
padding-right: 4px;
color: #c2c6dc;
&:hover {
background: rgba(115, 103, 240);
color: #c2c6dc;
}
`;
const HeaderSelect = styled.select`
text-decoration: underline;
font-size: 14px;
text-align: center;
padding: 4px 6px;
background: none;
outline: none;
border: none;
border-radius: 3px;
appearance: none;
&:hover {
background: #262c49;
border: 1px solid rgba(115, 103, 240);
outline: none !important;
box-shadow: none;
color: #c2c6dc;
}
&::-ms-expand {
display: none;
}
cursor: pointer;
position: absolute;
z-index: 9998;
margin: 0;
left: 0;
top: 5px;
opacity: 0;
`;
const HeaderButton = styled.button`
cursor: pointer;
color: #c2c6dc;
text-decoration: underline;
font-size: 14px;
text-align: center;
padding: 6px 10px;
margin: 6px 0;
background: none;
outline: none;
border: none;
border-radius: 3px;
&:hover {
background: rgba(115, 103, 240);
color: #fff;
}
`;
const HeaderActions = styled.div`
position: relative;
text-align: center;
& > button:first-child {
float: left;
}
& > button:last-child {
float: right;
}
`;
const DueDateManager: React.FC<DueDateManagerProps> = ({ task, onDueDateChange, onCancel }) => {
const now = moment();
const [textStartDate, setTextStartDate] = useState(now.format('YYYY-MM-DD'));
const [startDate, setStartDate] = useState(new Date());
useEffect(() => {
setTextStartDate(moment(startDate).format('YYYY-MM-DD'));
}, [startDate]);
const years = _.range(2010, getYear(new Date()) + 10, 1);
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const { register, handleSubmit, errors, setError, formState } = useForm<DueDateFormData>();
console.log(errors);
return (
<Wrapper>
<form>
<input
type="text"
id="endDate"
name="endDate"
onChange={e => {
setTextStartDate(e.currentTarget.value);
}}
value={textStartDate}
ref={register({
required: 'End due date is required.',
validate: value => {
const isValid = moment(value, 'YYYY-MM-DD').isValid();
console.log(`${value} - ${isValid}`);
return isValid;
},
})}
/>
</form>
<DueDatePickerWrapper>
<DatePicker inline selected={startDate} onChange={date => setStartDate(date ?? new Date())} />
<DatePicker
useWeekdaysShort
renderCustomHeader={({
date,
changeYear,
changeMonth,
decreaseMonth,
increaseMonth,
prevMonthButtonDisabled,
nextMonthButtonDisabled,
}) => (
<HeaderActions>
<HeaderButton onClick={decreaseMonth} disabled={prevMonthButtonDisabled}>
Prev
</HeaderButton>
<HeaderSelectLabel>
{months[date.getMonth()]}
<HeaderSelect value={getYear(date)} onChange={({ target: { value } }) => changeYear(parseInt(value))}>
{years.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderSelectLabel>
{date.getFullYear()}
<HeaderSelect
value={months[getMonth(date)]}
onChange={({ target: { value } }) => changeMonth(months.indexOf(value))}
>
{months.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</HeaderSelect>
</HeaderSelectLabel>
<HeaderButton onClick={increaseMonth} disabled={nextMonthButtonDisabled}>
Next
</HeaderButton>
</HeaderActions>
)}
selected={startDate}
inline
onChange={date => setStartDate(date ?? new Date())}
/>
</DueDatePickerWrapper>
<ActionWrapper>
<ConfirmAddDueDate onClick={() => onDueDateChange(task, startDate)}>Save</ConfirmAddDueDate>

View File

@ -64,6 +64,7 @@ export const HeaderEditTarget = styled.div<{ isHidden: boolean }>`
export const HeaderName = styled(TextareaAutosize)`
font-family: 'Droid Sans';
font-size: 14px;
border: none;
resize: none;
overflow: hidden;

View File

@ -171,6 +171,7 @@ export const ListsWithManyList = () => {
onCreateTask={action('card create')}
onTaskDrop={onCardDrop}
onTaskGroupDrop={onListDrop}
onChangeTaskGroupName={action('change group name')}
onCreateTaskGroup={action('create list')}
onExtraMenuOpen={action('extra menu open')}
onCardMemberClick={action('card member click')}

View File

@ -20,6 +20,7 @@ interface SimpleProps {
onTaskClick: (task: Task) => void;
onCreateTask: (taskGroupID: string, name: string) => void;
onChangeTaskGroupName: (taskGroupID: string, name: string) => void;
onQuickEditorOpen: (e: ContextMenuEvent) => void;
onCreateTaskGroup: (listName: string) => void;
onExtraMenuOpen: (taskGroupID: string, $targetRef: React.RefObject<HTMLElement>) => void;
@ -29,6 +30,7 @@ interface SimpleProps {
const SimpleLists: React.FC<SimpleProps> = ({
taskGroups,
onTaskDrop,
onChangeTaskGroupName,
onTaskGroupDrop,
onTaskClick,
onCreateTask,
@ -135,7 +137,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
name={taskGroup.name}
onOpenComposer={id => setCurrentComposer(id)}
isComposerOpen={currentComposer === taskGroup.id}
onSaveName={name => {}}
onSaveName={name => onChangeTaskGroupName(taskGroup.id, name)}
ref={columnDragProvided.innerRef}
wrapperProps={columnDragProvided.draggableProps}
headerProps={columnDragProvided.dragHandleProps}

View File

@ -101,3 +101,24 @@ export const RegisterButton = styled.button`
color: rgba(115, 103, 240);
cursor: pointer;
`;
export const LogoTitle = styled.div`
font-size: 24px;
font-weight: 600;
margin-left: 12px;
transition: visibility, opacity, transform 0.25s ease;
color: #7367f0;
`;
export const LogoWrapper = styled.div`
display: flex;
align-items: center;
justify-content: center;
flex-direction: row;
position: relative;
width: 100%;
padding-bottom: 16px;
margin-bottom: 24px;
color: rgb(222, 235, 255);
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
`;

View File

@ -1,10 +1,12 @@
import React, { useState } from 'react';
import AccessAccount from 'shared/undraw/AccessAccount';
import { User, Lock } from 'shared/icons';
import { User, Lock, Citadel } from 'shared/icons';
import { useForm } from 'react-hook-form';
import {
Form,
LogoWrapper,
LogoTitle,
ActionButtons,
RegisterButton,
LoginButton,
@ -35,6 +37,10 @@ const Login = ({ onSubmit }: LoginProps) => {
<Column>
<LoginFormWrapper>
<LoginFormContainer>
<LogoWrapper>
<Citadel size={42} />
<LogoTitle>Citadel</LogoTitle>
</LogoWrapper>
<Title>Login</Title>
<SubTitle>Welcome back, please login into your account.</SubTitle>
<Form onSubmit={handleSubmit(loginSubmit)}>

View File

@ -39,9 +39,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
<BoardMembersList>
{availableMembers
.filter(
member =>
currentSearch === '' ||
`${member.firstName} ${member.lastName}`.toLowerCase().startsWith(currentSearch.toLowerCase()),
member => currentSearch === '' || member.fullName.toLowerCase().startsWith(currentSearch.toLowerCase()),
)
.map(member => {
return (
@ -58,7 +56,7 @@ const MemberManager: React.FC<MemberManagerProps> = ({
}}
>
<ProfileIcon>JK</ProfileIcon>
<MemberName>{`${member.firstName} ${member.lastName}`}</MemberName>
<MemberName>{member.fullName}</MemberName>
{activeMembers.findIndex(m => m.id === member.id) !== -1 && (
<ActiveIconWrapper>
<Checkmark size={16} color="#42526e" />

View File

@ -227,8 +227,7 @@ export const MemberManagerPopup = () => {
availableMembers={[
{
id: '1',
firstName: 'Jordan',
lastName: 'Knott',
fullName: 'Jordan Knott',
profileIcon: { bgColor: null, url: null, initials: null },
},
]}
@ -293,8 +292,7 @@ export const DueDateManagerPopup = () => {
{
id: '1',
profileIcon: { bgColor: null, url: null, initials: null },
firstName: 'Jordan',
lastName: 'Knott',
fullName: 'Jordan Knott',
},
],
}}

View File

@ -0,0 +1,46 @@
import React, { useRef } from 'react';
import styled from 'styled-components';
export const Container = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
margin-left: 10px;
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
type ProfileIconProps = {
user: TaskUser;
onProfileClick: ($target: React.RefObject<HTMLElement>, user: TaskUser) => void;
size: number | string;
};
const ProfileIcon: React.FC<ProfileIconProps> = ({ user, onProfileClick, size }) => {
const $profileRef = useRef<HTMLDivElement>(null);
return (
<Container
ref={$profileRef}
onClick={() => {
onProfileClick($profileRef, user);
}}
size={size}
backgroundURL={user.profileIcon.url ?? null}
bgColor={user.profileIcon.bgColor ?? null}
>
{(!user.profileIcon.url && user.profileIcon.initials) ?? ''}
</Container>
);
};
ProfileIcon.defaultProps = {
size: 28,
};
export default ProfileIcon;

View File

@ -3,14 +3,14 @@ import TextareaAutosize from 'react-autosize-textarea';
import { mixin } from 'shared/utils/styles';
export const Wrapper = styled.div<{ open: boolean }>`
background: rgba(0, 0, 0, 0.4);
background: rgba(0, 0, 0, 0.55);
bottom: 0;
color: #fff;
left: 0;
position: fixed;
right: 0;
top: 0;
z-index: 30;
z-index: 100;
visibility: ${props => (props.open ? 'show' : 'hidden')};
`;

View File

@ -0,0 +1,30 @@
import React, { createRef, useState } from 'react';
import { action } from '@storybook/addon-actions';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Settings from '.';
export default {
component: Settings,
title: 'Settings',
parameters: {
backgrounds: [
{ name: 'white', value: '#ffffff', default: true },
{ name: 'gray', value: '#f8f8f8' },
],
},
};
const profile = { url: 'http://localhost:3333/uploads/headshot.png', bgColor: '#000', initials: 'JK' };
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Settings
profile={profile}
onProfileAvatarRemove={action('remove')}
onProfileAvatarChange={action('profile avatar change')}
/>
</>
);
};

View File

@ -0,0 +1,362 @@
import React, { useState, useRef } from 'react';
import styled from 'styled-components';
import { User } from 'shared/icons';
const TextFieldWrapper = styled.div`
display: flex;
align-items: flex-start;
flex-direction: column;
position: relative;
justify-content: center;
margin-bottom: 2.2rem;
margin-top: 17px;
`;
const TextFieldLabel = styled.span`
padding: 0.7rem !important;
color: #c2c6dc;
left: 0;
top: 0;
transition: all 0.2s ease;
position: absolute;
border-radius: 5px;
overflow: hidden;
font-size: 0.85rem;
cursor: text;
width: 100%;
font-size: 12px;
user-select: none;
pointer-events: none;
}
`;
const TextFieldInput = styled.input`
font-size: 12px;
border: 1px solid rgba(0, 0, 0, 0.2);
background: #262c49;
padding: 0.7rem !important;
color: #c2c6dc;
position: relative;
border-radius: 5px;
transition: all 0.3s ease;
width: 100%;
&:focus {
box-shadow: 0 3px 10px 0 rgba(0, 0, 0, 0.15);
border: 1px solid rgba(115, 103, 240);
}
&:focus ~ ${TextFieldLabel} {
color: rgba(115, 103, 240);
transform: translate(-3px, -90%);
}
`;
type TextFieldProps = {
label: string;
};
const TextField: React.FC<TextFieldProps> = ({ label }) => {
return (
<TextFieldWrapper>
<TextFieldInput />
<TextFieldLabel>{label}</TextFieldLabel>
</TextFieldWrapper>
);
};
const ProfileContainer = styled.div`
display: flex;
align-items: center;
margin-bottom: 2.2rem !important;
`;
const AvatarContainer = styled.div`
width: 70px;
height: 70px;
border-radius: 50%;
position: relative;
cursor: pointer;
display: inline-block;
margin: 5px;
margin-bottom: 1rem;
margin-right: 1rem;
`;
const AvatarMask = styled.div<{ background: string }>`
position: absolute;
width: 100%;
height: 100%;
overflow: hidden;
background: ${props => props.background};
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
`;
const AvatarImg = styled.img<{ src: string }>`
display: block;
width: 100%;
height: 100%;
`;
const ActionButtons = styled.div`
display: flex;
flex-wrap: wrap;
align-items: center;
`;
const UploadButton = styled.div`
margin-right: 1rem;
padding: 0.75rem 2rem;
border: 0;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
color: #fff;
display: inline-block;
background: rgba(115, 103, 240);
`;
const RemoveButton = styled.button`
display: inline-block;
border: 1px solid rgba(234, 84, 85, 1);
background: transparent;
color: rgba(234, 84, 85, 1);
padding: 0.75rem 2rem;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
`;
const ImgLabel = styled.p`
color: #c2c6dc;
margin-top: 0.5rem;
font-size: 12.25px;
width: 100%;
`;
const AvatarInitials = styled.span`
font-size: 32px;
color: #fff;
`;
type AvatarSettingsProps = {
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
profile: ProfileIcon;
};
const AvatarSettings: React.FC<AvatarSettingsProps> = ({ profile, onProfileAvatarChange, onProfileAvatarRemove }) => {
return (
<ProfileContainer>
<AvatarContainer>
<AvatarMask
background={profile.url ? 'none' : profile.bgColor ?? 'none'}
onClick={() => onProfileAvatarChange()}
>
{profile.url ? (
<AvatarImg alt="" src={profile.url ?? ''} />
) : (
<AvatarInitials>{profile.initials}</AvatarInitials>
)}
</AvatarMask>
</AvatarContainer>
<ActionButtons>
<UploadButton onClick={() => onProfileAvatarChange()}>Upload photo</UploadButton>
<RemoveButton onClick={() => onProfileAvatarRemove()}>Remove</RemoveButton>
<ImgLabel>Allowed JPG, GIF or PNG. Max size of 800kB</ImgLabel>
</ActionButtons>
</ProfileContainer>
);
};
const Container = styled.div`
padding: 2.2rem;
display: flex;
width: 100%;
position: relative;
`;
const TabNav = styled.div`
float: left;
width: 220px;
height: 100%;
display: block;
position: relative;
`;
const TabNavContent = styled.ul`
display: block;
width: auto;
border-bottom: 0 !important;
border-right: 1px solid rgba(0, 0, 0, 0.05);
`;
const TabNavItem = styled.li`
padding: 0.35rem 0.3rem;
display: block;
position: relative;
`;
const TabNavItemButton = styled.button<{ active: boolean }>`
cursor: pointer;
display: flex;
align-items: center;
padding-top: 10px !important;
padding-bottom: 10px !important;
padding-left: 12px !important;
padding-right: 8px !important;
width: 100%;
position: relative;
color: ${props => (props.active ? 'rgba(115, 103, 240)' : '#c2c6dc')};
&:hover {
color: rgba(115, 103, 240);
}
&:hover svg {
fill: rgba(115, 103, 240);
}
`;
const TabNavItemSpan = styled.span`
text-align: left;
padding-left: 9px;
font-size: 14px;
`;
const TabNavLine = styled.span<{ top: number }>`
left: auto;
right: 0;
width: 2px;
height: 48px;
transform: scaleX(1);
top: ${props => props.top}px;
background: linear-gradient(30deg, rgba(115, 103, 240), rgba(115, 103, 240));
box-shadow: 0 0 8px 0 rgba(115, 103, 240);
display: block;
position: absolute;
transition: all 0.2s ease;
`;
const TabContentWrapper = styled.div`
position: relative;
display: block;
overflow: hidden;
width: 100%;
`;
const TabContent = styled.div`
position: relative;
width: 100%;
display: block;
padding: 0;
padding: 1.5rem;
background-color: #10163a;
margin-left: 1rem !important;
border-radius: 0.5rem;
`;
const TabContentInner = styled.div``;
const items = [{ name: 'General' }, { name: 'Change Password' }, { name: 'Info' }, { name: 'Notifications' }];
type NavItemProps = {
active: boolean;
name: string;
tab: number;
onClick: (tab: number, top: number) => void;
};
const NavItem: React.FC<NavItemProps> = ({ active, name, tab, onClick }) => {
const $item = useRef<HTMLLIElement>(null);
return (
<TabNavItem
key={name}
ref={$item}
onClick={() => {
if ($item && $item.current) {
const pos = $item.current.getBoundingClientRect();
onClick(tab, pos.top);
}
}}
>
<TabNavItemButton active={active}>
<User size={14} color={active ? 'rgba(115, 103, 240)' : '#c2c6dc'} />
<TabNavItemSpan>{name}</TabNavItemSpan>
</TabNavItemButton>
</TabNavItem>
);
};
const SettingActions = styled.div`
display: flex;
align-items: center;
justify-content: flex-end;
`;
const SaveButton = styled.div`
margin-right: 1rem;
padding: 0.75rem 2rem;
border: 0;
border-radius: 6px;
cursor: pointer;
position: relative;
overflow: hidden;
color: #fff;
display: inline-block;
background: rgba(115, 103, 240);
`;
type SettingsProps = {
onProfileAvatarChange: () => void;
onProfileAvatarRemove: () => void;
profile: ProfileIcon;
};
const Settings: React.FC<SettingsProps> = ({ onProfileAvatarRemove, onProfileAvatarChange, profile }) => {
const [currentTab, setTab] = useState(0);
const [currentTop, setTop] = useState(0);
const $tabNav = useRef<HTMLDivElement>(null);
return (
<Container>
<TabNav ref={$tabNav}>
<TabNavContent>
{items.map((item, idx) => (
<NavItem
onClick={(tab, top) => {
if ($tabNav && $tabNav.current) {
const pos = $tabNav.current.getBoundingClientRect();
setTab(tab);
setTop(top - pos.top);
}
}}
name={item.name}
tab={idx}
active={idx === currentTab}
/>
))}
<TabNavLine top={currentTop} />
</TabNavContent>
</TabNav>
<TabContentWrapper>
<TabContent>
<AvatarSettings
onProfileAvatarRemove={onProfileAvatarRemove}
onProfileAvatarChange={onProfileAvatarChange}
profile={profile}
/>
<TextField label="Name" />
<TextField label="Initials " />
<TextField label="Username " />
<TextField label="Email" />
<TextField label="Bio" />
<SettingActions>
<SaveButton>Save Change</SaveButton>
</SettingActions>
</TabContent>
</TabContentWrapper>
</Container>
);
};
export default Settings;

View File

@ -0,0 +1,25 @@
import React, { useRef } from 'react';
import NormalizeStyles from 'App/NormalizeStyles';
import BaseStyles from 'App/BaseStyles';
import Tabs from '.';
export default {
component: Tabs,
title: 'Tabs',
parameters: {
backgrounds: [
{ name: 'gray', value: '#f8f8f8', default: true },
{ name: 'white', value: '#ffffff' },
],
},
};
export const Default = () => {
return (
<>
<NormalizeStyles />
<BaseStyles />
<Tabs />
</>
);
};

View File

@ -0,0 +1,8 @@
import React from 'react';
import styled from 'styled-components';
const Tabs = () => {
return <span>HEllo!</span>;
};
export default Tabs;

View File

@ -8,7 +8,7 @@ const TaskDetailAssignee = styled.div`
margin-right: 4px;
`;
const ProfileIcon = styled.div<{ size: string | number }>`
export const Wrapper = styled.div<{ size: number | string; bgColor: string | null; backgroundURL: string | null }>`
width: ${props => props.size}px;
height: ${props => props.size}px;
border-radius: 9999px;
@ -16,10 +16,10 @@ const ProfileIcon = styled.div<{ size: string | number }>`
align-items: center;
justify-content: center;
color: #fff;
font-weight: 400;
background: rgb(115, 103, 240);
font-size: 14px;
cursor: pointer;
font-weight: 700;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
type TaskAssigneeProps = {
@ -31,8 +31,17 @@ type TaskAssigneeProps = {
const TaskAssignee: React.FC<TaskAssigneeProps> = ({ member, onMemberProfile, size }) => {
const $memberRef = useRef<HTMLDivElement>(null);
return (
<TaskDetailAssignee ref={$memberRef} onClick={() => onMemberProfile($memberRef, member.id)} key={member.id}>
<ProfileIcon size={size}>{member.profileIcon.initials ?? ''}</ProfileIcon>
<TaskDetailAssignee
ref={$memberRef}
onClick={e => {
e.stopPropagation();
onMemberProfile($memberRef, member.id);
}}
key={member.id}
>
<Wrapper backgroundURL={member.profileIcon.url ?? null} bgColor={member.profileIcon.bgColor ?? null} size={size}>
{(!member.profileIcon.url && member.profileIcon.initials) ?? ''}
</Wrapper>
</TaskDetailAssignee>
);
};

View File

@ -55,8 +55,7 @@ export const Default = () => {
{
id: '1',
profileIcon: { bgColor: null, url: null, initials: null },
firstName: 'Jordan',
lastName: 'Knott',
fullName: 'Jordan Knott',
},
],
}}

View File

@ -68,7 +68,7 @@ export const ProfileNameSecondary = styled.small`
color: #c2c6dc;
`;
export const ProfileIcon = styled.div<{ bgColor: string }>`
export const ProfileIcon = styled.div<{ bgColor: string | null; backgroundURL: string | null }>`
margin-left: 10px;
width: 40px;
height: 40px;
@ -78,8 +78,9 @@ export const ProfileIcon = styled.div<{ bgColor: string }>`
justify-content: center;
color: #fff;
font-weight: 700;
background: ${props => props.bgColor};
cursor: pointer;
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
background-position: center;
background-size: contain;
`;
export const ProjectMeta = styled.div`

View File

@ -21,42 +21,25 @@ export default {
};
export const Default = () => {
const [menu, setMenu] = useState({
top: 0,
left: 0,
isOpen: false,
});
const onClick = (bottom: number, right: number) => {
setMenu({
isOpen: !menu.isOpen,
left: right,
top: bottom,
});
};
return (
<>
<NormalizeStyles />
<BaseStyles />
<TopNavbar
projectName="Projects"
bgColor="#7367F0"
firstName="Jordan"
lastName="Knott"
initials="JK"
user={{
id: '1',
fullName: 'Jordan Knott',
profileIcon: {
url: null,
initials: 'JK',
bgColor: '#000',
},
}}
onNotificationClick={action('notifications click')}
onOpenSettings={action('open settings')}
onProfileClick={onClick}
onProfileClick={action('profile click')}
/>
{menu.isOpen && (
<DropdownMenu
onCloseDropdown={() => {
setMenu({ left: 0, top: 0, isOpen: false });
}}
onLogout={action('on logout')}
left={menu.left}
top={menu.top}
/>
)}
</>
);
};

View File

@ -1,6 +1,6 @@
import React, { useRef, useState, useEffect } from 'react';
import { Star, Ellipsis, Bell, Cog, AngleDown } from 'shared/icons';
import ProfileIcon from 'shared/components/ProfileIcon';
import {
NotificationContainer,
ProjectNameTextarea,
@ -18,7 +18,6 @@ import {
Breadcrumbs,
BreadcrumpSeparator,
ProjectSettingsButton,
ProfileIcon,
ProfileContainer,
ProfileNameWrapper,
ProfileNamePrimary,
@ -110,14 +109,11 @@ const ProjectHeading: React.FC<ProjectHeadingProps> = ({
type NavBarProps = {
projectName: string | null;
onProfileClick: (bottom: number, right: number) => void;
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
onSaveProjectName?: (projectName: string) => void;
onNotificationClick: () => void;
bgColor: string;
user: TaskUser | null;
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
firstName: string;
lastName: string;
initials: string;
projectMembers?: Array<TaskUser> | null;
};
@ -126,17 +122,14 @@ const NavBar: React.FC<NavBarProps> = ({
onSaveProjectName,
onProfileClick,
onNotificationClick,
firstName,
lastName,
initials,
bgColor,
user,
projectMembers,
onOpenSettings,
}) => {
const $profileRef: any = useRef(null);
const handleProfileClick = () => {
const boundingRect = $profileRef.current.getBoundingClientRect();
onProfileClick(boundingRect.bottom, boundingRect.right);
const handleProfileClick = ($target: React.RefObject<HTMLElement>) => {
if ($target && $target.current) {
onProfileClick($target);
}
};
const { showPopup } = usePopup();
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
@ -189,15 +182,16 @@ const NavBar: React.FC<NavBarProps> = ({
<NotificationContainer onClick={onNotificationClick}>
<Bell color="#c2c6dc" size={20} />
</NotificationContainer>
<ProfileContainer>
<ProfileNameWrapper>
<ProfileNamePrimary>{`${firstName} ${lastName}`}</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper>
<ProfileIcon ref={$profileRef} onClick={handleProfileClick} bgColor={bgColor}>
{initials}
</ProfileIcon>
</ProfileContainer>
{user && (
<ProfileContainer>
<ProfileNameWrapper>
<ProfileNamePrimary>{user.fullName}</ProfileNamePrimary>
<ProfileNameSecondary>Manager</ProfileNameSecondary>
</ProfileNameWrapper>
<ProfileIcon user={user} size={40} onProfileClick={handleProfileClick} />}
</ProfileContainer>
)}
</GlobalActions>
</NavbarHeader>
</NavbarWrapper>

View File

@ -11,10 +11,12 @@ export type Scalars = {
Float: number;
Time: any;
UUID: string;
Upload: any;
};
export type ProjectLabel = {
__typename?: 'ProjectLabel';
id: Scalars['ID'];
@ -48,8 +50,7 @@ export type ProfileIcon = {
export type ProjectMember = {
__typename?: 'ProjectMember';
id: Scalars['ID'];
firstName: Scalars['String'];
lastName: Scalars['String'];
fullName: Scalars['String'];
profileIcon: ProfileIcon;
};
@ -66,8 +67,8 @@ export type UserAccount = {
id: Scalars['ID'];
email: Scalars['String'];
createdAt: Scalars['Time'];
firstName: Scalars['String'];
lastName: Scalars['String'];
fullName: Scalars['String'];
initials: Scalars['String'];
username: Scalars['String'];
profileIcon: ProfileIcon;
};
@ -169,8 +170,8 @@ export type NewRefreshToken = {
export type NewUserAccount = {
username: Scalars['String'];
email: Scalars['String'];
firstName: Scalars['String'];
lastName: Scalars['String'];
fullName: Scalars['String'];
initials: Scalars['String'];
password: Scalars['String'];
};
@ -314,6 +315,7 @@ export type Mutation = {
createRefreshToken: RefreshToken;
createUserAccount: UserAccount;
createTeam: Team;
clearProfileAvatar: UserAccount;
createProject: Project;
updateProjectName: Project;
createProjectLabel: ProjectLabel;
@ -470,11 +472,26 @@ export type AssignTaskMutation = (
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
)> }
) }
);
export type ClearProfileAvatarMutationVariables = {};
export type ClearProfileAvatarMutation = (
{ __typename?: 'Mutation' }
& { clearProfileAvatar: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
) }
) }
);
export type CreateProjectMutationVariables = {
teamID: Scalars['UUID'];
userID: Scalars['UUID'];
@ -541,7 +558,7 @@ export type CreateTaskMutation = (
) }
)>, assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -624,7 +641,7 @@ export type FindProjectQuery = (
& Pick<Project, 'name'>
& { members: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -658,7 +675,7 @@ export type FindProjectQuery = (
) }
)>, assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -698,7 +715,7 @@ export type FindTaskQuery = (
) }
)>, assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'url' | 'initials' | 'bgColor'>
@ -732,10 +749,10 @@ export type MeQuery = (
{ __typename?: 'Query' }
& { me: (
{ __typename?: 'UserAccount' }
& Pick<UserAccount, 'firstName' | 'lastName'>
& Pick<UserAccount, 'id' | 'fullName'>
& { profileIcon: (
{ __typename?: 'ProfileIcon' }
& Pick<ProfileIcon, 'initials' | 'bgColor'>
& Pick<ProfileIcon, 'initials' | 'bgColor' | 'url'>
) }
) }
);
@ -783,7 +800,7 @@ export type UnassignTaskMutation = (
& Pick<Task, 'id'>
& { assigned: Array<(
{ __typename?: 'ProjectMember' }
& Pick<ProjectMember, 'id' | 'firstName' | 'lastName'>
& Pick<ProjectMember, 'id' | 'fullName'>
)> }
) }
);
@ -831,7 +848,7 @@ export type UpdateTaskDescriptionMutation = (
{ __typename?: 'Mutation' }
& { updateTaskDescription: (
{ __typename?: 'Task' }
& Pick<Task, 'id'>
& Pick<Task, 'id' | 'description'>
) }
);
@ -893,8 +910,7 @@ export const AssignTaskDocument = gql`
id
assigned {
id
firstName
lastName
fullName
}
}
}
@ -925,6 +941,43 @@ export function useAssignTaskMutation(baseOptions?: ApolloReactHooks.MutationHoo
export type AssignTaskMutationHookResult = ReturnType<typeof useAssignTaskMutation>;
export type AssignTaskMutationResult = ApolloReactCommon.MutationResult<AssignTaskMutation>;
export type AssignTaskMutationOptions = ApolloReactCommon.BaseMutationOptions<AssignTaskMutation, AssignTaskMutationVariables>;
export const ClearProfileAvatarDocument = gql`
mutation clearProfileAvatar {
clearProfileAvatar {
id
fullName
profileIcon {
initials
bgColor
url
}
}
}
`;
export type ClearProfileAvatarMutationFn = ApolloReactCommon.MutationFunction<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
/**
* __useClearProfileAvatarMutation__
*
* To run a mutation, you first call `useClearProfileAvatarMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useClearProfileAvatarMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [clearProfileAvatarMutation, { data, loading, error }] = useClearProfileAvatarMutation({
* variables: {
* },
* });
*/
export function useClearProfileAvatarMutation(baseOptions?: ApolloReactHooks.MutationHookOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>) {
return ApolloReactHooks.useMutation<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>(ClearProfileAvatarDocument, baseOptions);
}
export type ClearProfileAvatarMutationHookResult = ReturnType<typeof useClearProfileAvatarMutation>;
export type ClearProfileAvatarMutationResult = ApolloReactCommon.MutationResult<ClearProfileAvatarMutation>;
export type ClearProfileAvatarMutationOptions = ApolloReactCommon.BaseMutationOptions<ClearProfileAvatarMutation, ClearProfileAvatarMutationVariables>;
export const CreateProjectDocument = gql`
mutation createProject($teamID: UUID!, $userID: UUID!, $name: String!) {
createProject(input: {teamID: $teamID, userID: $userID, name: $name}) {
@ -1035,8 +1088,7 @@ export const CreateTaskDocument = gql`
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials
@ -1219,8 +1271,7 @@ export const FindProjectDocument = gql`
name
members {
id
firstName
lastName
fullName
profileIcon {
url
initials
@ -1269,8 +1320,7 @@ export const FindProjectDocument = gql`
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials
@ -1341,8 +1391,7 @@ export const FindTaskDocument = gql`
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials
@ -1423,11 +1472,12 @@ export type GetProjectsQueryResult = ApolloReactCommon.QueryResult<GetProjectsQu
export const MeDocument = gql`
query me {
me {
firstName
lastName
id
fullName
profileIcon {
initials
bgColor
url
}
}
}
@ -1513,8 +1563,7 @@ export const UnassignTaskDocument = gql`
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
id
firstName
lastName
fullName
}
id
}
@ -1626,6 +1675,7 @@ export const UpdateTaskDescriptionDocument = gql`
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
id
description
}
}
`;

View File

@ -3,8 +3,7 @@ mutation assignTask($taskID: UUID!, $userID: UUID!) {
id
assigned {
id
firstName
lastName
fullName
}
}
}

View File

@ -0,0 +1,11 @@
mutation clearProfileAvatar {
clearProfileAvatar {
id
fullName
profileIcon {
initials
bgColor
url
}
}
}

View File

@ -26,8 +26,7 @@ mutation createTask($taskGroupID: String!, $name: String!, $position: Float!) {
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials

View File

@ -3,8 +3,7 @@ query findProject($projectId: String!) {
name
members {
id
firstName
lastName
fullName
profileIcon {
url
initials
@ -53,8 +52,7 @@ query findProject($projectId: String!) {
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials

View File

@ -24,8 +24,7 @@ query findTask($taskID: UUID!) {
}
assigned {
id
firstName
lastName
fullName
profileIcon {
url
initials

View File

@ -1,10 +1,11 @@
query me {
me {
firstName
lastName
id
fullName
profileIcon {
initials
bgColor
url
}
}
}

View File

@ -2,8 +2,7 @@ mutation unassignTask($taskID: UUID!, $userID: UUID!) {
unassignTask(input: {taskID: $taskID, userID: $userID}) {
assigned {
id
firstName
lastName
fullName
}
id
}

View File

@ -1,5 +1,6 @@
mutation updateTaskDescription($taskID: UUID!, $description: String!) {
updateTaskDescription(input: {taskID: $taskID, description: $description}) {
id
description
}
}

View File

@ -2971,6 +2971,13 @@
resolved "https://registry.yarnpkg.com/@types/anymatch/-/anymatch-1.3.1.tgz#336badc1beecb9dacc38bea2cf32adf627a8421a"
integrity sha512-/+CRPXpBDpo2RK9C68N3b2cOvO0Cf5B9aPijHsoDQTHivnGSObdOF2BRQOYjojWTDy6nQvMjmqRXIxH55VjxxA==
"@types/axios@^0.14.0":
version "0.14.0"
resolved "https://registry.yarnpkg.com/@types/axios/-/axios-0.14.0.tgz#ec2300fbe7d7dddd7eb9d3abf87999964cafce46"
integrity sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=
dependencies:
axios "*"
"@types/babel-types@*", "@types/babel-types@^7.0.0":
version "7.0.7"
resolved "https://registry.yarnpkg.com/@types/babel-types/-/babel-types-7.0.7.tgz#667eb1640e8039436028055737d2b9986ee336e3"
@ -3035,6 +3042,13 @@
dependencies:
"@types/color-convert" "*"
"@types/date-fns@^2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@types/date-fns/-/date-fns-2.6.0.tgz#b062ca46562002909be0c63a6467ed173136acc1"
integrity sha1-sGLKRlYgApCb4MY6ZGftFzE2rME=
dependencies:
date-fns "*"
"@types/eslint-visitor-keys@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d"
@ -3724,6 +3738,18 @@ adjust-sourcemap-loader@2.0.0:
object-path "0.11.4"
regex-parser "2.2.10"
ag-grid-community@^23.2.0:
version "23.2.0"
resolved "https://registry.yarnpkg.com/ag-grid-community/-/ag-grid-community-23.2.0.tgz#889f52e8eb91c167c2ac7477938cbf498a54f67c"
integrity sha512-aG7Ghfu79HeqOCd50GhFSeZUX1Tw9BVUX1VKMuglkAcwYPTQjuYvYT7QVQB5FGzfFjcVq4a1QFfcgdoAcZYJIA==
ag-grid-react@^23.2.0:
version "23.2.0"
resolved "https://registry.yarnpkg.com/ag-grid-react/-/ag-grid-react-23.2.0.tgz#00a54cb4e83c0d35a49c202e5833e3bb20d8cfa8"
integrity sha512-lDGV+WX0Nj5biNOJRSErFehXG+nqkbuXPMS7YJxEDWJLJxtOF0INP5sL6dtxV12j/XHqXa+M2CgQBXZWZq+EWg==
dependencies:
prop-types "^15.6.2"
agent-base@4, agent-base@^4.3.0:
version "4.3.0"
resolved "https://registry.yarnpkg.com/agent-base/-/agent-base-4.3.0.tgz#8165f01c436009bccad0b1d122f05ed770efc6ee"
@ -4399,6 +4425,18 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e"
integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug==
axios-auth-refresh@^2.2.7:
version "2.2.7"
resolved "https://registry.yarnpkg.com/axios-auth-refresh/-/axios-auth-refresh-2.2.7.tgz#922f458129ed653d9bd0d732743bf9bba4524f12"
integrity sha512-5gdrwRG3luW/BHIwyh7vZk9AFkC3tOkWhE4yJJW0Dno36kcTHN8V87SxH52m3HF8bQpORaV3RNmuwlOT8pKOOw==
axios@*, axios@^0.19.2:
version "0.19.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.2.tgz#3ea36c5d8818d0d5f8a8a97a6d36b86cdc00cb27"
integrity sha512-fjgm5MvRHLhx+osE2xoekY70AhARk3a6hkN+3Io1jc00jtquGvxYlKlsFUhmUET0V5te6CcZI7lcv2Ym61mjHA==
dependencies:
follow-redirects "1.5.10"
axios@0.19.0:
version "0.19.0"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.0.tgz#8e09bff3d9122e133f7b8101c8fbdd00ed3d2ab8"
@ -6351,6 +6389,11 @@ data-urls@^1.0.0, data-urls@^1.1.0:
whatwg-mimetype "^2.2.0"
whatwg-url "^7.0.0"
date-fns@*, date-fns@^2.14.0:
version "2.14.0"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.14.0.tgz#359a87a265bb34ef2e38f93ecf63ac453f9bc7ba"
integrity sha512-1zD+68jhFgDIM0rF05rcwYO8cExdNqxjq4xP1QKM60Q45mnO6zaMWB4tOzrIr4M4GSLntsKeE4c9Bdl2jhL/yw==
date-fns@^1.27.2:
version "1.30.1"
resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c"