diff --git a/client/src/components/layout/MemberList/index.tsx b/client/src/components/layout/MemberList/index.tsx index b2bca577ec205946fc3e062d86e4597023e086dc..0a91de0b774502bc58c36b580e2f631b2ce0fbc8 100644 --- a/client/src/components/layout/MemberList/index.tsx +++ b/client/src/components/layout/MemberList/index.tsx @@ -1,14 +1,9 @@ import './member-list.scss'; import TeamMember from 'components/ui/TeamMember'; - -interface TeamMemberInterface { - uuid: string; - name: string; - role: string; -} +import { TeamMember as ITeamMember } from 'adapters/team'; interface Props { - members: TeamMemberInterface[] + members: ITeamMember[] } export default function MemberList({ members }: Props) { @@ -18,7 +13,7 @@ export default function MemberList({ members }: Props) { + </div> {members.map((member) => ( - <TeamMember key={member.uuid} member={member} /> + <TeamMember key={member.id} member={member} /> ))} </div> ); diff --git a/client/src/components/navigation/Dropdown/dropdown.scss b/client/src/components/navigation/Dropdown/dropdown.scss index 3c04a83c6d330add76af99347b59a7a79bce8e93..ec63bac756a2ba601b78e3307fe29d8b704f17ae 100644 --- a/client/src/components/navigation/Dropdown/dropdown.scss +++ b/client/src/components/navigation/Dropdown/dropdown.scss @@ -15,6 +15,13 @@ margin-right: 5px; } } + + &.inactive { + cursor: text; + .icon { + display: none; + } + } &.open { .dropdown { diff --git a/client/src/components/navigation/Dropdown/index.tsx b/client/src/components/navigation/Dropdown/index.tsx index 13bc318274f9ca73d646234c0ca1f60b338f2aa9..4643f306b5a626ddbacce10228451d6144c525a9 100644 --- a/client/src/components/navigation/Dropdown/index.tsx +++ b/client/src/components/navigation/Dropdown/index.tsx @@ -2,7 +2,7 @@ import { ReactNode, useState } from 'react'; import { Link } from 'react-router-dom'; import './dropdown.scss'; -interface DropDownItem { +export interface DropDownItem { label: string; route: string; } @@ -15,19 +15,21 @@ interface Props { export default function Dropdown({ children, items }: Props) { const [isOpen, setOpen] = useState(false); return ( - <div className={'dropdown-container' + (isOpen ? ' open' : '')} onClick={() => setOpen(state => !state)}> + <div className={'dropdown-container' + (isOpen ? ' open' : '') + (items.length === 0 ? ' inactive' : '')} onClick={() => setOpen(state => !state)}> <div className="current-item"> {children} </div> - <div className="dropdown"> - { - items.map((item) => ( - <Link className="dropdown-item" key={item.label} to={item.route}> - {item.label} - </Link> - )) - } - </div> + {items.length > 0 && ( + <div className="dropdown"> + { + items.map((item) => ( + <Link className="dropdown-item" key={item.label} to={item.route}> + {item.label} + </Link> + )) + } + </div> + )} </div> ); } \ No newline at end of file diff --git a/client/src/components/navigation/Tabs/index.tsx b/client/src/components/navigation/Tabs/index.tsx index 897cf81c436d9df010fb70b48ac4432690d74eaf..60e497d991987d545a507c3d311fe2faed10b61b 100644 --- a/client/src/components/navigation/Tabs/index.tsx +++ b/client/src/components/navigation/Tabs/index.tsx @@ -1,9 +1,11 @@ -import { NavLink, RouteProps, Route } from 'react-router-dom'; +import { ReactNode } from 'react'; +import { NavLink, Route } from 'react-router-dom'; import './tabs.scss'; -interface Tab extends RouteProps { +export interface Tab { label: string; - routePath?: string; + route: string; + component: ReactNode } interface Props { @@ -16,7 +18,7 @@ export default function Tabs({ tabs }: Props) { <> <nav className="tabs-container"> {tabs.map((tab) => ( - <NavLink key={tab.label} className="tab" exact activeClassName="active" to={{ pathname: tab.path?.toString() }}> + <NavLink key={tab.label} className="tab" exact activeClassName="active" to={tab.route}> {tab.label} </NavLink> ))} @@ -24,7 +26,7 @@ export default function Tabs({ tabs }: Props) { </nav> { tabs.map((tab) => ( - <Route exact key={tab.label} {...tab} path={tab.routePath ?? tab.path} /> + <Route exact key={tab.label} path={tab.route} render={() => (tab.component)} /> )) } </> diff --git a/client/src/components/ui/Avatar/index.tsx b/client/src/components/ui/Avatar/index.tsx index ea6dba569b5ab55bf797d61e4650cfea166b55e9..fdbbf7e90f360ec50de457db46cae844f3ed64d0 100644 --- a/client/src/components/ui/Avatar/index.tsx +++ b/client/src/components/ui/Avatar/index.tsx @@ -25,7 +25,7 @@ export default function Avatar({ user }: Props) { { error && ( <div className="standard-image"> - {user?.username.charAt(0)} + {user?.username && user.username.charAt(0)} </div> ) } diff --git a/client/src/components/ui/TeamMember/index.tsx b/client/src/components/ui/TeamMember/index.tsx index 5b5d4ec2cf4f71f7341c4304a5e42aeaf82dd69d..6060a843e7e6279027b1c5a3c61e081d514e109d 100644 --- a/client/src/components/ui/TeamMember/index.tsx +++ b/client/src/components/ui/TeamMember/index.tsx @@ -1,28 +1,21 @@ import './team-member.scss'; -import avatar from 'images/roland-bernard.jpg'; - -interface TeamMemberInterface { - name: string; - role: string; -} +import { TeamMember as ITeamMember } from 'adapters/team'; +import Avatar from 'components/ui/Avatar'; interface Props { - member: TeamMemberInterface + member: ITeamMember } export default function TeamMember({ member }: Props) { return ( - <div className="team-member-item"> - <div className="avatar-container"> - <img src={avatar} alt={member.name} /> - </div> + <Avatar user={member} /> <div className="details"> - <div className="name">{member.name}</div> - <div className="role">{member.role}</div> + <div className="name">{member.username}</div> + <div className="role">{member.role.name}</div> </div> <div className="settings"> - + </div> </div> ); diff --git a/client/src/components/ui/TeamMember/team-member.scss b/client/src/components/ui/TeamMember/team-member.scss index 6a4db6ae4f8a9f3496a7a88a791f9112437893ae..612a3059bc1d1d9cd9f83e4d15646e6a1435d133 100644 --- a/client/src/components/ui/TeamMember/team-member.scss +++ b/client/src/components/ui/TeamMember/team-member.scss @@ -8,10 +8,11 @@ background: s.$white; border-radius: 15px; box-shadow: 0 0 10px rgba(s.$black, 0.1); - .avatar-container { + .avatar { width: 60px; height: 60px; transform: translateX(-50%); + font-size: 36px; img { width: 100%; border-radius: 50%; diff --git a/client/src/pages/AppWrapper.tsx b/client/src/pages/AppWrapper.tsx index 564827c94f6174cd700156538a84ce47fdb26017..d2dbc2db8ebf46335c070526a527af64145a6d69 100644 --- a/client/src/pages/AppWrapper.tsx +++ b/client/src/pages/AppWrapper.tsx @@ -29,7 +29,7 @@ export default function AppWrapper() { <ProtectedRoute path="/stats" component={Stats} /> <ProtectedRoute path="/settings" component={Settings} /> <ProtectedRoute path="/teams/:uuid/edit" exact component={TeamsEdit} /> - <ProtectedRoute path="/teams" component={Teams} /> + <ProtectedRoute path={['/teams/:teamId', '/teams']} component={Teams} /> </Switch> </Suspense> </Header> diff --git a/client/src/pages/Projects/ProjectDetail/index.tsx b/client/src/pages/Projects/ProjectDetail/index.tsx index cb69c97933a9128752051887fd5e3aa13998784a..3885bd1608dd2e0eb73aa5feec6cf1513c26d582 100644 --- a/client/src/pages/Projects/ProjectDetail/index.tsx +++ b/client/src/pages/Projects/ProjectDetail/index.tsx @@ -35,7 +35,7 @@ export default function ProjectDetail() { A basic shopping list app, much like Bring! </p> </div> - <Tabs tabs={tabs} /> + {/*<Tabs tabs={tabs} />*/} </div> </div> diff --git a/client/src/pages/Tasks/TaskDetail/TaskAssignees/index.tsx b/client/src/pages/Tasks/TaskDetail/TaskAssignees/index.tsx index 2d2f749f4b227197c82041b002c5019e60960ce6..58116b75ca0168462eda4f22869f512d9c48a81f 100644 --- a/client/src/pages/Tasks/TaskDetail/TaskAssignees/index.tsx +++ b/client/src/pages/Tasks/TaskDetail/TaskAssignees/index.tsx @@ -1,14 +1,14 @@ +import { TeamMember } from 'adapters/team'; import MemberList from 'components/layout/MemberList'; -export default function TaskAssignees() { - const member = { - uuid: 'asdf', - name: 'Roland Bernard', - role: 'Backend' - } +interface Props { + assignees: TeamMember[] +} + +export default function TaskAssignees({assignees}: Props) { return ( <section className="teams-assignees-section"> - <MemberList members={[member]} /> + <MemberList members={assignees} /> </section> ); } \ No newline at end of file diff --git a/client/src/pages/Tasks/TaskDetail/index.tsx b/client/src/pages/Tasks/TaskDetail/index.tsx index 96cb73f6688238fe51dbfdf1e57833916b92ebfc..e41ffe1034e3d4a777cd4e915b0984a5e470285e 100644 --- a/client/src/pages/Tasks/TaskDetail/index.tsx +++ b/client/src/pages/Tasks/TaskDetail/index.tsx @@ -52,7 +52,7 @@ export default function TaskDetail() { <ButtonLink href={'/tasks/' + uuid + '/edit'} className="dark expanded"> Edit </ButtonLink> - <Tabs tabs={tabs} /> + {/*<Tabs tabs={tabs} /> */} </div> </div> ); diff --git a/client/src/pages/Teams/TeamsMembers/index.tsx b/client/src/pages/Teams/TeamsMembers/index.tsx index fdb4fe3860481cd805acebd562c5c0fe388280a9..4f18e465dcf7a5276f8c95ed83ce102b0ab4e465 100644 --- a/client/src/pages/Teams/TeamsMembers/index.tsx +++ b/client/src/pages/Teams/TeamsMembers/index.tsx @@ -1,15 +1,15 @@ import './teams-members.scss'; import MemberList from 'components/layout/MemberList'; +import { TeamMember } from 'adapters/team'; -export default function TeamsMembers() { - const member = { - uuid: 'asdf', - name: 'Roland Bernard', - role: 'Backend' - } +interface Props { + members: TeamMember[]; +} + +export default function TeamsMembers({ members }: Props) { return ( <section className="teams-members-section"> - <MemberList members={[member]} /> + <MemberList members={members} /> </section> ) } \ No newline at end of file diff --git a/client/src/pages/Teams/index.tsx b/client/src/pages/Teams/index.tsx index f146bc266c2c4a93505dd7d8348af7c24bd0c736..4956749fefb0277f7c40778e6533a726a0086179 100644 --- a/client/src/pages/Teams/index.tsx +++ b/client/src/pages/Teams/index.tsx @@ -1,66 +1,88 @@ import './teams.scss'; import DetailGrid from 'components/layout/DetailGrid'; import ButtonLink from 'components/navigation/ButtonLink'; -import Tabs from 'components/navigation/Tabs'; -import Dropdown from 'components/navigation/Dropdown'; +import Tabs, { Tab } from 'components/navigation/Tabs'; +import Dropdown, { DropDownItem } from 'components/navigation/Dropdown'; import TeamsMembers from './TeamsMembers'; import TeamsStats from './TeamsStats'; import { useEffect, useState } from 'react'; import { getTeamMembers, getTeamProjects, getTeams, Team } from 'adapters/team'; import { DetailProps } from 'components/ui/DetailBox'; +import { useHistory, useParams } from 'react-router'; + +export interface Params { + teamId: string; +} export default function Teams() { + const { teamId } = useParams<Params>(); + const history = useHistory(); const [allTeams, setTeams] = useState<Team[]>(); const [currentTeam, setCurrentTeam] = useState<Team>(); const [details, setDetails] = useState<DetailProps[]>([]); + const [tabs, setTabs] = useState<Tab[]>([]); + const [pageLinks, setPageLinks] = useState<DropDownItem[]>([]); + useEffect(() => { getTeams().then((teams) => { - setTeams(teams); - setCurrentTeam(teams[0]); - if (teams[0]) { - getTeamProjects(teams[0].id).then((projects) => { - console.log(projects); - setDetails((state) => [...state, { - icon: 'folder', - title: 'Projects', - number: projects.length - }]); - }); - getTeamMembers(teams[0].id).then((members) => { - setDetails((state) => [...state, { - icon: 'group', - title: 'Members', - number: members.length - }]); - }) + //if no team is defined, take the first one + if (!teamId && teams[0]) { + history.push('/teams/' + teams[0].id); } + setTeams(teams); + setCurrentTeam(teams.find(team => team.id === teamId)); }).catch(() => { }); - }, []); + }, [teamId, history]); + + useEffect(() => { + if (currentTeam && allTeams) { + setDetails([]); + getTeamProjects(currentTeam.id).then((projects) => { + setDetails((state) => [...state, { + icon: 'folder', + title: 'Projects', + number: projects.length + }]); + }); + getTeamMembers(currentTeam.id).then((members) => { - const tabs = [{ - path: '/teams', - label: 'Members', - component: TeamsMembers - }, { - path: '/teams/stats', - label: 'Stats', - component: TeamsStats - }]; + setDetails((state) => [...state, { + icon: 'group', + title: 'Members', + number: members.length + }]); + + setTabs([{ + route: '/teams/' + currentTeam.id, + label: 'Members', + component: (<TeamsMembers members={members}/>) + }, { + route: '/teams/' + currentTeam.id + '/stats', + label: 'Stats', + component: <TeamsStats /> + }]); + + }); + + //update Tabs link + setPageLinks(allTeams.filter(team => currentTeam.id !== team.id).map(team => { + return { + route: '/teams' + team.id, + label: team.name, + } + })); + } + }, [currentTeam, allTeams]) - //TODO dynamic - const teams = [{ - route: '/teams/members?team=someOther', - label: 'Some other Team', - }]; return ( <div className="teams-page"> <div className="content-container"> <h1 className="underlined">Teams</h1> { - allTeams && allTeams?.length > 1 && ( - <Dropdown items={teams}> + allTeams && ( + <Dropdown items={pageLinks}> <h2>{currentTeam?.name}</h2> <span className="material-icons icon"> expand_more @@ -68,11 +90,6 @@ export default function Teams() { </Dropdown> ) } - { - allTeams && allTeams?.length === 1 && ( - <h2>{currentTeam?.name}</h2> - ) - } <DetailGrid details={details} /> <ButtonLink href={'/teams/' + currentTeam?.id + '/edit'} className="expanded"> Edit diff --git a/server/src/v1/team.ts b/server/src/v1/team.ts index a12609eb7adbbcddafef31b9ec3a00a43630a9cb..a006f6ee9f40dac9c63805f5e0696b3ef007d564 100644 --- a/server/src/v1/team.ts +++ b/server/src/v1/team.ts @@ -203,7 +203,7 @@ team.get('/:uuid/members', async (req, res) => { status: 'success', members: members.map(member => ({ id: member.id, - name: member.name, + username: member.name, email: member.email, realname: member.realname, role: {