arch: move web folder into api & move api to top level
This commit is contained in:
263
frontend/src/shared/components/TopNavbar/Styles.ts
Normal file
263
frontend/src/shared/components/TopNavbar/Styles.ts
Normal file
@ -0,0 +1,263 @@
|
||||
import styled, { css } from 'styled-components';
|
||||
import TextareaAutosize from 'react-autosize-textarea';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import Button from 'shared/components/Button';
|
||||
import { Citadel } from 'shared/icons';
|
||||
import { NavLink, Link } from 'react-router-dom';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
export const ProjectMember = styled(TaskAssignee)<{ zIndex: number }>`
|
||||
z-index: ${props => props.zIndex};
|
||||
position: relative;
|
||||
`;
|
||||
|
||||
export const NavbarWrapper = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const ProjectMembers = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
export const NavbarHeader = styled.header`
|
||||
height: 80px;
|
||||
padding: 0 1.75rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: rgb(16, 22, 58);
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
|
||||
border-bottom: 1px solid rgba(65, 69, 97, 0.65);
|
||||
`;
|
||||
export const Breadcrumbs = styled.div`
|
||||
color: rgb(94, 108, 132);
|
||||
font-size: 15px;
|
||||
`;
|
||||
export const BreadcrumpSeparator = styled.span`
|
||||
position: relative;
|
||||
top: 2px;
|
||||
font-size: 18px;
|
||||
margin: 0px 10px;
|
||||
`;
|
||||
|
||||
export const ProjectActions = styled.div`
|
||||
flex: 1;
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 1px;
|
||||
`;
|
||||
|
||||
export const GlobalActions = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ProfileContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const ProfileNameWrapper = styled.div`
|
||||
text-align: right;
|
||||
line-height: 1.25;
|
||||
`;
|
||||
|
||||
export const IconContainer = styled.div<{ disabled?: boolean }>`
|
||||
margin-right: 20px;
|
||||
cursor: pointer;
|
||||
${props =>
|
||||
props.disabled &&
|
||||
css`
|
||||
opacity: 0.5;
|
||||
cursor: default;
|
||||
pointer-events: none;
|
||||
`}
|
||||
`;
|
||||
|
||||
export const ProfileNamePrimary = styled.div`
|
||||
color: #c2c6dc;
|
||||
font-weight: 600;
|
||||
`;
|
||||
|
||||
export const ProfileNameSecondary = styled.small`
|
||||
color: #c2c6dc;
|
||||
`;
|
||||
|
||||
export const ProfileIcon = styled.div<{ bgColor: string | null; backgroundURL: string | null }>`
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
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;
|
||||
`;
|
||||
|
||||
export const ProjectMeta = styled.div<{ nameOnly?: boolean }>`
|
||||
display: flex;
|
||||
${props => !props.nameOnly && 'padding-top: 9px;'}
|
||||
margin-left: -14px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
min-height: 51px;
|
||||
`;
|
||||
|
||||
export const ProjectTabs = styled.div`
|
||||
align-items: flex-end;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
justify-content: flex-start;
|
||||
max-width: 100%;
|
||||
`;
|
||||
|
||||
export const ProjectTab = styled(NavLink)`
|
||||
font-size: 80%;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
line-height: normal;
|
||||
min-width: 1px;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: box-shadow, color;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
padding-bottom: 12px;
|
||||
|
||||
&:not(:last-child) {
|
||||
margin-right: 20px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.text.secondary});
|
||||
color: rgba(${props => props.theme.colors.text.secondary});
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
|
||||
color: rgba(${props => props.theme.colors.secondary});
|
||||
}
|
||||
&.active:hover {
|
||||
box-shadow: inset 0 -2px rgba(${props => props.theme.colors.secondary});
|
||||
color: rgba(${props => props.theme.colors.secondary});
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProjectName = styled.h1`
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
padding: 3px 10px 3px 8px;
|
||||
margin: -4px 0;
|
||||
`;
|
||||
export const ProjectNameTextarea = styled(TextareaAutosize)`
|
||||
border: none;
|
||||
resize: none;
|
||||
overflow: hidden;
|
||||
overflow-wrap: break-word;
|
||||
background: transparent;
|
||||
border-radius: 3px;
|
||||
box-shadow: none;
|
||||
margin: -4px 0;
|
||||
|
||||
letter-spacing: normal;
|
||||
word-spacing: normal;
|
||||
text-transform: none;
|
||||
text-indent: 0px;
|
||||
text-shadow: none;
|
||||
flex-direction: column;
|
||||
text-align: start;
|
||||
|
||||
color: #c2c6dc;
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
padding: 3px 10px 3px 8px;
|
||||
&:focus {
|
||||
box-shadow: rgb(115, 103, 240) 0px 0px 0px 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProjectSwitcher = styled.button`
|
||||
font-size: 20px;
|
||||
|
||||
outline: none;
|
||||
border: none;
|
||||
width: 100px;
|
||||
border-radius: 3px;
|
||||
line-height: 20px;
|
||||
padding: 6px 4px;
|
||||
background-color: none;
|
||||
text-align: center;
|
||||
color: #c2c6dc;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
}
|
||||
`;
|
||||
|
||||
export const Separator = styled.div`
|
||||
color: #c2c6dc;
|
||||
font-size: 20px;
|
||||
padding-left: 4px;
|
||||
padding-right: 4px;
|
||||
`;
|
||||
|
||||
export const ProjectSettingsButton = styled.button`
|
||||
outline: none;
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
line-height: 20px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
background-color: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: rgb(115, 103, 240);
|
||||
}
|
||||
`;
|
||||
|
||||
export const InviteButton = styled(Button)`
|
||||
margin: 0 0 0 8px;
|
||||
padding: 6px 12px;
|
||||
`;
|
||||
|
||||
export const ProjectFinder = styled(Button)`
|
||||
margin-right: 20px;
|
||||
padding: 6px 12px;
|
||||
`;
|
||||
|
||||
export const NavSeparator = styled.div`
|
||||
width: 1px;
|
||||
background: rgba(${props => props.theme.colors.border});
|
||||
height: 34px;
|
||||
margin: 0 20px;
|
||||
`;
|
||||
|
||||
export const LogoContainer = styled(Link)`
|
||||
display: block;
|
||||
left: 50%;
|
||||
position: absolute;
|
||||
transform: translateX(-50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
`;
|
||||
|
||||
export const CitadelTitle = styled.h2`
|
||||
margin-left: 5px;
|
||||
color: rgba(${props => props.theme.colors.text.primary});
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
export const CitadelLogo = styled(Citadel)`
|
||||
fill: rgba(${props => props.theme.colors.text.primary});
|
||||
stroke: rgba(${props => props.theme.colors.text.primary});
|
||||
`;
|
@ -0,0 +1,49 @@
|
||||
import React, { useState } from 'react';
|
||||
import NormalizeStyles from 'App/NormalizeStyles';
|
||||
import BaseStyles from 'App/BaseStyles';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
import DropdownMenu from 'shared/components/DropdownMenu';
|
||||
import TopNavbar from '.';
|
||||
|
||||
export default {
|
||||
component: TopNavbar,
|
||||
title: 'TopNavbar',
|
||||
|
||||
// Our exports that end in "Data" are not stories.
|
||||
excludeStories: /.*Data$/,
|
||||
parameters: {
|
||||
backgrounds: [
|
||||
{ name: 'white', value: '#ffffff' },
|
||||
{ name: 'gray', value: '#f8f8f8' },
|
||||
{ name: 'darkBlue', value: '#262c49', default: true },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
<>
|
||||
<NormalizeStyles />
|
||||
<BaseStyles />
|
||||
<TopNavbar
|
||||
onOpenProjectFinder={action('finder')}
|
||||
name="Projects"
|
||||
user={{
|
||||
id: '1',
|
||||
fullName: 'Jordan Knott',
|
||||
profileIcon: {
|
||||
url: null,
|
||||
initials: 'JK',
|
||||
bgColor: '#000',
|
||||
},
|
||||
}}
|
||||
onChangeRole={action('change role')}
|
||||
onNotificationClick={action('notifications click')}
|
||||
onOpenSettings={action('open settings')}
|
||||
onDashboardClick={action('open dashboard')}
|
||||
onRemoveFromBoard={action('remove project')}
|
||||
onProfileClick={action('profile click')}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
312
frontend/src/shared/components/TopNavbar/index.tsx
Normal file
312
frontend/src/shared/components/TopNavbar/index.tsx
Normal file
@ -0,0 +1,312 @@
|
||||
import React, { useRef, useState, useEffect } from 'react';
|
||||
import { Home, Star, Bell, AngleDown, BarChart, CheckCircle } from 'shared/icons';
|
||||
import styled from 'styled-components';
|
||||
import ProfileIcon from 'shared/components/ProfileIcon';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import { usePopup, Popup } from 'shared/components/PopupMenu';
|
||||
import { RoleCode } from 'shared/generated/graphql';
|
||||
import MiniProfile from 'shared/components/MiniProfile';
|
||||
import {
|
||||
CitadelLogo,
|
||||
CitadelTitle,
|
||||
ProjectFinder,
|
||||
LogoContainer,
|
||||
NavSeparator,
|
||||
IconContainer,
|
||||
ProjectNameTextarea,
|
||||
InviteButton,
|
||||
GlobalActions,
|
||||
ProjectActions,
|
||||
ProjectMeta,
|
||||
ProjectName,
|
||||
ProjectTabs,
|
||||
ProjectTab,
|
||||
NavbarWrapper,
|
||||
NavbarHeader,
|
||||
ProjectSettingsButton,
|
||||
ProfileContainer,
|
||||
ProfileNameWrapper,
|
||||
ProfileNamePrimary,
|
||||
ProfileNameSecondary,
|
||||
ProjectMember,
|
||||
ProjectMembers,
|
||||
} from './Styles';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
const HomeDashboard = styled(Home)``;
|
||||
|
||||
type ProjectHeadingProps = {
|
||||
onFavorite?: () => void;
|
||||
name: string;
|
||||
onSaveProjectName?: (projectName: string) => void;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
};
|
||||
|
||||
const ProjectHeading: React.FC<ProjectHeadingProps> = ({
|
||||
onFavorite,
|
||||
name: initialProjectName,
|
||||
onSaveProjectName,
|
||||
onOpenSettings,
|
||||
}) => {
|
||||
const [isEditProjectName, setEditProjectName] = useState(false);
|
||||
const [projectName, setProjectName] = useState(initialProjectName);
|
||||
const $projectName = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
if (isEditProjectName && $projectName && $projectName.current) {
|
||||
$projectName.current.focus();
|
||||
$projectName.current.select();
|
||||
}
|
||||
}, [isEditProjectName]);
|
||||
useEffect(() => {
|
||||
setProjectName(initialProjectName);
|
||||
}, [initialProjectName]);
|
||||
|
||||
const onProjectNameChange = (event: React.FormEvent<HTMLTextAreaElement>): void => {
|
||||
setProjectName(event.currentTarget.value);
|
||||
};
|
||||
const onProjectNameBlur = () => {
|
||||
if (onSaveProjectName) {
|
||||
onSaveProjectName(projectName);
|
||||
}
|
||||
setEditProjectName(false);
|
||||
};
|
||||
const onProjectNameKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
if ($projectName && $projectName.current) {
|
||||
$projectName.current.blur();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const $settings = useRef<HTMLButtonElement>(null);
|
||||
return (
|
||||
<>
|
||||
{isEditProjectName ? (
|
||||
<ProjectNameTextarea
|
||||
ref={$projectName}
|
||||
onChange={onProjectNameChange}
|
||||
onKeyDown={onProjectNameKeyDown}
|
||||
onBlur={onProjectNameBlur}
|
||||
spellCheck={false}
|
||||
value={projectName}
|
||||
/>
|
||||
) : (
|
||||
<ProjectName
|
||||
onClick={() => {
|
||||
setEditProjectName(true);
|
||||
}}
|
||||
>
|
||||
{projectName}
|
||||
</ProjectName>
|
||||
)}
|
||||
<ProjectSettingsButton
|
||||
onClick={() => {
|
||||
onOpenSettings($settings);
|
||||
}}
|
||||
ref={$settings}
|
||||
>
|
||||
<AngleDown color="#c2c6dc" />
|
||||
</ProjectSettingsButton>
|
||||
{onFavorite && (
|
||||
<ProjectSettingsButton onClick={() => onFavorite()}>
|
||||
<Star width={16} height={16} color="#c2c6dc" />
|
||||
</ProjectSettingsButton>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export type MenuItem = {
|
||||
name: string;
|
||||
link: string;
|
||||
};
|
||||
type MenuTypes = {
|
||||
[key: string]: Array<string>;
|
||||
};
|
||||
|
||||
export const MENU_TYPES: MenuTypes = {
|
||||
PROJECT_MENU: ['Board', 'Timeline', 'Calender'],
|
||||
TEAM_MENU: ['Projects', 'Members', 'Settings'],
|
||||
};
|
||||
|
||||
type NavBarProps = {
|
||||
menuType?: Array<MenuItem> | null;
|
||||
name: string | null;
|
||||
currentTab?: number;
|
||||
onSetTab?: (tab: number) => void;
|
||||
onOpenProjectFinder: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onChangeProjectOwner?: (userID: string) => void;
|
||||
onChangeRole?: (userID: string, roleCode: RoleCode) => void;
|
||||
onFavorite?: () => void;
|
||||
onProfileClick: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onSaveName?: (name: string) => void;
|
||||
onNotificationClick: () => void;
|
||||
onInviteUser?: ($target: React.RefObject<HTMLElement>) => void;
|
||||
onDashboardClick: () => void;
|
||||
user: TaskUser | null;
|
||||
onOpenSettings: ($target: React.RefObject<HTMLElement>) => void;
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
};
|
||||
|
||||
const NavBar: React.FC<NavBarProps> = ({
|
||||
menuType,
|
||||
onInviteUser,
|
||||
onChangeProjectOwner,
|
||||
currentTab,
|
||||
onOpenProjectFinder,
|
||||
onFavorite,
|
||||
onSetTab,
|
||||
onChangeRole,
|
||||
name,
|
||||
onRemoveFromBoard,
|
||||
onSaveName,
|
||||
onProfileClick,
|
||||
onNotificationClick,
|
||||
onDashboardClick,
|
||||
user,
|
||||
projectMembers,
|
||||
onOpenSettings,
|
||||
}) => {
|
||||
const handleProfileClick = ($target: React.RefObject<HTMLElement>) => {
|
||||
if ($target && $target.current) {
|
||||
onProfileClick($target);
|
||||
}
|
||||
};
|
||||
const { showPopup } = usePopup();
|
||||
const onMemberProfile = ($targetRef: React.RefObject<HTMLElement>, memberID: string) => {
|
||||
const member = projectMembers ? projectMembers.find(u => u.id === memberID) : null;
|
||||
const warning =
|
||||
'You can’t leave because you are the only admin. To make another user an admin, click their avatar, select “Change permissions…”, and select “Admin”.';
|
||||
if (member) {
|
||||
console.log(member);
|
||||
showPopup(
|
||||
$targetRef,
|
||||
<MiniProfile
|
||||
warning={member.role && member.role.code === 'owner' ? warning : null}
|
||||
onChangeProjectOwner={
|
||||
member.role && member.role.code !== 'owner'
|
||||
? (userID: string) => {
|
||||
if (user && onChangeProjectOwner) {
|
||||
onChangeProjectOwner(userID);
|
||||
}
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
canChangeRole={member.role && member.role.code !== 'owner'}
|
||||
onChangeRole={roleCode => {
|
||||
if (onChangeRole) {
|
||||
onChangeRole(member.id, roleCode);
|
||||
}
|
||||
}}
|
||||
onRemoveFromBoard={
|
||||
member.role && member.role.code === 'owner'
|
||||
? undefined
|
||||
: () => {
|
||||
if (onRemoveFromBoard) {
|
||||
onRemoveFromBoard(member.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
user={member}
|
||||
bio=""
|
||||
/>,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<NavbarWrapper>
|
||||
<NavbarHeader>
|
||||
<ProjectActions>
|
||||
<ProjectMeta>
|
||||
{name && (
|
||||
<ProjectHeading
|
||||
onFavorite={onFavorite}
|
||||
onOpenSettings={onOpenSettings}
|
||||
name={name}
|
||||
onSaveProjectName={onSaveName}
|
||||
/>
|
||||
)}
|
||||
</ProjectMeta>
|
||||
{name && (
|
||||
<ProjectTabs>
|
||||
{menuType &&
|
||||
menuType.map((menu, idx) => {
|
||||
return (
|
||||
<ProjectTab
|
||||
key={menu.name}
|
||||
to={menu.link}
|
||||
exact
|
||||
onClick={() => {
|
||||
// TODO
|
||||
}}
|
||||
>
|
||||
{menu.name}
|
||||
</ProjectTab>
|
||||
);
|
||||
})}
|
||||
</ProjectTabs>
|
||||
)}
|
||||
</ProjectActions>
|
||||
<LogoContainer to="/">
|
||||
<CitadelLogo width={24} height={24} />
|
||||
<CitadelTitle>Citadel</CitadelTitle>
|
||||
</LogoContainer>
|
||||
<GlobalActions>
|
||||
{projectMembers && (
|
||||
<>
|
||||
<ProjectMembers>
|
||||
{projectMembers.map((member, idx) => (
|
||||
<ProjectMember
|
||||
showRoleIcons
|
||||
zIndex={projectMembers.length - idx}
|
||||
key={member.id}
|
||||
size={28}
|
||||
member={member}
|
||||
onMemberProfile={onMemberProfile}
|
||||
/>
|
||||
))}
|
||||
<InviteButton
|
||||
onClick={$target => {
|
||||
if (onInviteUser) {
|
||||
onInviteUser($target);
|
||||
}
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
Invite
|
||||
</InviteButton>
|
||||
</ProjectMembers>
|
||||
<NavSeparator />
|
||||
</>
|
||||
)}
|
||||
<ProjectFinder onClick={onOpenProjectFinder} variant="gradient">
|
||||
Projects
|
||||
</ProjectFinder>
|
||||
<IconContainer onClick={onDashboardClick}>
|
||||
<HomeDashboard width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled>
|
||||
<CheckCircle width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled>
|
||||
<BarChart width={20} height={20} />
|
||||
</IconContainer>
|
||||
|
||||
{user && (
|
||||
<IconContainer>
|
||||
<ProfileIcon user={user} size={30} onProfileClick={handleProfileClick} />
|
||||
</IconContainer>
|
||||
)}
|
||||
</GlobalActions>
|
||||
</NavbarHeader>
|
||||
</NavbarWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default NavBar;
|
Reference in New Issue
Block a user