feat: add bell notification system for task assignment
This commit is contained in:
@@ -225,7 +225,7 @@ const Card = React.forwardRef(
|
||||
<ListCardBadges>
|
||||
{watched && (
|
||||
<ListCardBadge>
|
||||
<Eye width={8} height={8} />
|
||||
<Eye width={12} height={12} />
|
||||
</ListCardBadge>
|
||||
)}
|
||||
{dueDate && (
|
||||
|
||||
@@ -329,6 +329,7 @@ const SimpleLists: React.FC<SimpleProps> = ({
|
||||
toggleLabels={toggleLabels}
|
||||
isPublic={isPublic}
|
||||
labelVariant={cardLabelVariant}
|
||||
watched={task.watched}
|
||||
wrapperProps={{
|
||||
...taskProvided.draggableProps,
|
||||
...taskProvided.dragHandleProps,
|
||||
|
||||
@@ -1,8 +1,20 @@
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
import React, { useState } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
import TimeAgo from 'react-timeago';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { mixin } from 'shared/utils/styles';
|
||||
import {
|
||||
useNotificationsQuery,
|
||||
NotificationFilter,
|
||||
ActionType,
|
||||
useNotificationAddedSubscription,
|
||||
useNotificationToggleReadMutation,
|
||||
} from 'shared/generated/graphql';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import { Popup } from 'shared/components/PopupMenu';
|
||||
import { Popup, usePopup } from 'shared/components/PopupMenu';
|
||||
import { CheckCircleOutline, Circle, CircleSolid, UserCircle } from 'shared/icons';
|
||||
import produce from 'immer';
|
||||
|
||||
const ItemWrapper = styled.div`
|
||||
cursor: pointer;
|
||||
@@ -37,7 +49,7 @@ const ItemTextContainer = styled.div`
|
||||
const ItemTextTitle = styled.span`
|
||||
font-weight: 500;
|
||||
display: block;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
color: ${(props) => props.theme.colors.primary};
|
||||
font-size: 14px;
|
||||
`;
|
||||
const ItemTextDesc = styled.span`
|
||||
@@ -72,38 +84,450 @@ export const NotificationItem: React.FC<NotificationItemProps> = ({ title, descr
|
||||
};
|
||||
|
||||
const NotificationHeader = styled.div`
|
||||
padding: 0.75rem;
|
||||
padding: 20px 28px;
|
||||
text-align: center;
|
||||
border-top-left-radius: 6px;
|
||||
border-top-right-radius: 6px;
|
||||
background: ${props => props.theme.colors.primary};
|
||||
background: ${(props) => props.theme.colors.primary};
|
||||
`;
|
||||
|
||||
const NotificationHeaderTitle = styled.span`
|
||||
font-size: 14px;
|
||||
color: ${props => props.theme.colors.text.secondary};
|
||||
color: ${(props) => props.theme.colors.text.secondary};
|
||||
`;
|
||||
|
||||
const Notifications = styled.div`
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-color: #414561;
|
||||
height: 448px;
|
||||
overflow-y: scroll;
|
||||
user-select: none;
|
||||
`;
|
||||
const NotificationFooter = styled.div`
|
||||
cursor: pointer;
|
||||
padding: 0.5rem;
|
||||
text-align: center;
|
||||
color: ${props => props.theme.colors.primary};
|
||||
color: ${(props) => props.theme.colors.primary};
|
||||
&:hover {
|
||||
background: ${props => props.theme.colors.bg.primary};
|
||||
background: ${(props) => props.theme.colors.bg.primary};
|
||||
}
|
||||
border-bottom-left-radius: 6px;
|
||||
border-bottom-right-radius: 6px;
|
||||
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-color: #414561;
|
||||
`;
|
||||
|
||||
const NotificationTabs = styled.div`
|
||||
align-items: flex-end;
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
flex: 1 0 auto;
|
||||
justify-content: flex-start;
|
||||
max-width: 100%;
|
||||
padding-top: 4px;
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-color: #414561;
|
||||
`;
|
||||
|
||||
const NotificationTab = styled.div<{ active: boolean }>`
|
||||
font-size: 80%;
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
user-select: none;
|
||||
|
||||
justify-content: center;
|
||||
line-height: normal;
|
||||
min-width: 1px;
|
||||
transition-duration: 0.2s;
|
||||
transition-property: box-shadow, color;
|
||||
white-space: nowrap;
|
||||
flex: 0 1 auto;
|
||||
padding: 12px 16px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 12px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
|
||||
color: ${(props) => props.theme.colors.text.secondary};
|
||||
}
|
||||
&:not(:last-child) {
|
||||
margin-right: 12px;
|
||||
}
|
||||
|
||||
${(props) =>
|
||||
props.active &&
|
||||
css`
|
||||
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
|
||||
color: ${props.theme.colors.secondary};
|
||||
&:hover {
|
||||
box-shadow: inset 0 -2px ${props.theme.colors.secondary};
|
||||
color: ${props.theme.colors.secondary};
|
||||
}
|
||||
`}
|
||||
`;
|
||||
|
||||
const NotificationLink = styled(Link)`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text-decoration: none;
|
||||
padding: 16px 8px;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const NotificationControls = styled.div`
|
||||
width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
visibility: hidden;
|
||||
padding: 4px;
|
||||
`;
|
||||
|
||||
const NotificationButtons = styled.div`
|
||||
display: flex;
|
||||
align-self: flex-end;
|
||||
align-items: center;
|
||||
margin-top: auto;
|
||||
margin-bottom: 6px;
|
||||
`;
|
||||
|
||||
const NotificationButton = styled.div`
|
||||
padding: 4px 15px;
|
||||
cursor: pointer;
|
||||
&:hover svg {
|
||||
fill: rgb(216, 93, 216);
|
||||
stroke: rgb(216, 93, 216);
|
||||
}
|
||||
`;
|
||||
|
||||
const NotificationWrapper = styled.li`
|
||||
min-height: 112px;
|
||||
display: flex;
|
||||
font-size: 14px;
|
||||
transition: background-color 0.1s ease-in-out;
|
||||
margin: 2px 8px;
|
||||
border-radius: 8px;
|
||||
justify-content: space-between;
|
||||
position: relative;
|
||||
&:hover {
|
||||
background: ${(props) => mixin.rgba(props.theme.colors.primary, 0.5)};
|
||||
}
|
||||
&:hover ${NotificationLink} {
|
||||
color: #fff;
|
||||
}
|
||||
&:hover ${NotificationControls} {
|
||||
visibility: visible;
|
||||
}
|
||||
`;
|
||||
|
||||
const NotificationContentFooter = styled.div`
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
`;
|
||||
|
||||
const NotificationCausedBy = styled.div`
|
||||
height: 60px;
|
||||
width: 60px;
|
||||
min-height: 60px;
|
||||
min-width: 60px;
|
||||
`;
|
||||
const NotificationCausedByInitials = styled.div`
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
text: #fff;
|
||||
font-size: 18px;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #7367f0;
|
||||
`;
|
||||
|
||||
const NotificationCausedByImage = styled.img`
|
||||
position: relative;
|
||||
display: flex;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
border: none;
|
||||
background: #7367f0;
|
||||
`;
|
||||
|
||||
const NotificationContent = styled.div`
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
margin-left: 16px;
|
||||
`;
|
||||
|
||||
const NotificationContentHeader = styled.div`
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
|
||||
svg {
|
||||
margin-left: 8px;
|
||||
fill: rgb(216, 93, 216);
|
||||
stroke: rgb(216, 93, 216);
|
||||
}
|
||||
`;
|
||||
|
||||
const NotificationBody = styled.div`
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #fff;
|
||||
svg {
|
||||
fill: rgb(216, 93, 216);
|
||||
stroke: rgb(216, 93, 216);
|
||||
}
|
||||
`;
|
||||
|
||||
const NotificationPrefix = styled.span`
|
||||
color: rgb(216, 93, 216);
|
||||
margin: 0 4px;
|
||||
`;
|
||||
|
||||
const NotificationSeparator = styled.span`
|
||||
margin: 0 6px;
|
||||
`;
|
||||
|
||||
type NotificationProps = {
|
||||
causedBy?: { fullname: string; username: string; id: string } | null;
|
||||
createdAt: string;
|
||||
read: boolean;
|
||||
data: Array<{ key: string; value: string }>;
|
||||
actionType: ActionType;
|
||||
onToggleRead: () => void;
|
||||
};
|
||||
|
||||
const Notification: React.FC<NotificationProps> = ({ causedBy, createdAt, data, actionType, read, onToggleRead }) => {
|
||||
const prefix: any = [];
|
||||
const { hidePopup } = usePopup();
|
||||
const dataMap = new Map<string, string>();
|
||||
data.forEach((d) => dataMap.set(d.key, d.value));
|
||||
let link = '#';
|
||||
switch (actionType) {
|
||||
case ActionType.TaskAssigned:
|
||||
prefix.push(<UserCircle width={14} height={16} />);
|
||||
prefix.push(<NotificationPrefix>Assigned </NotificationPrefix>);
|
||||
prefix.push(<span>you to the task "{dataMap.get('TaskName')}"</span>);
|
||||
link = `/projects/${dataMap.get('ProjectID')}/board/c/${dataMap.get('TaskID')}`;
|
||||
break;
|
||||
default:
|
||||
throw new Error('unknown action type');
|
||||
}
|
||||
|
||||
return (
|
||||
<NotificationWrapper>
|
||||
<NotificationLink to={link} onClick={hidePopup}>
|
||||
<NotificationCausedBy>
|
||||
<NotificationCausedByInitials>
|
||||
{causedBy
|
||||
? causedBy.fullname
|
||||
.split(' ')
|
||||
.map((n) => n[0])
|
||||
.join('.')
|
||||
: 'RU'}
|
||||
</NotificationCausedByInitials>
|
||||
</NotificationCausedBy>
|
||||
<NotificationContent>
|
||||
<NotificationContentHeader>
|
||||
{causedBy ? causedBy.fullname : 'Removed user'}
|
||||
{!read && <CircleSolid width={10} height={10} />}
|
||||
</NotificationContentHeader>
|
||||
<NotificationBody>{prefix}</NotificationBody>
|
||||
<NotificationContentFooter>
|
||||
<span>{dayjs.duration(dayjs(createdAt).diff(dayjs())).humanize(true)}</span>
|
||||
<NotificationSeparator>•</NotificationSeparator>
|
||||
<span>{dataMap.get('ProjectName')}</span>
|
||||
</NotificationContentFooter>
|
||||
</NotificationContent>
|
||||
</NotificationLink>
|
||||
<NotificationControls>
|
||||
<NotificationButtons>
|
||||
<NotificationButton onClick={() => onToggleRead()}>
|
||||
{read ? <Circle width={18} height={18} /> : <CheckCircleOutline width={18} height={18} />}
|
||||
</NotificationButton>
|
||||
</NotificationButtons>
|
||||
</NotificationControls>
|
||||
</NotificationWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
const PopupContent = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-left: 1px solid rgba(0, 0, 0, 0.1);
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
padding-bottom: 10px;
|
||||
border-color: #414561;
|
||||
`;
|
||||
|
||||
const tabs = [
|
||||
{ label: 'All', key: NotificationFilter.All },
|
||||
{ label: 'Unread', key: NotificationFilter.Unread },
|
||||
{ label: 'I was mentioned', key: NotificationFilter.Mentioned },
|
||||
{ label: 'Assigned to me', key: NotificationFilter.Assigned },
|
||||
];
|
||||
|
||||
type NotificationEntry = {
|
||||
id: string;
|
||||
read: boolean;
|
||||
readAt?: string | undefined | null;
|
||||
notification: {
|
||||
id: string;
|
||||
data: Array<{ key: string; value: string }>;
|
||||
actionType: ActionType;
|
||||
causedBy?: { id: string; username: string; fullname: string } | undefined | null;
|
||||
createdAt: string;
|
||||
};
|
||||
};
|
||||
const NotificationPopup: React.FC = ({ children }) => {
|
||||
const [filter, setFilter] = useState<NotificationFilter>(NotificationFilter.Unread);
|
||||
const [data, setData] = useState<{ nodes: Array<NotificationEntry>; hasNextPage: boolean; cursor: string }>({
|
||||
nodes: [],
|
||||
hasNextPage: false,
|
||||
cursor: '',
|
||||
});
|
||||
const [toggleRead] = useNotificationToggleReadMutation({
|
||||
onCompleted: (data) => {
|
||||
setData((prev) => {
|
||||
return produce(prev, (draft) => {
|
||||
const idx = draft.nodes.findIndex((n) => n.id === data.notificationToggleRead.id);
|
||||
if (idx !== -1) {
|
||||
draft.nodes[idx].read = data.notificationToggleRead.read;
|
||||
draft.nodes[idx].readAt = data.notificationToggleRead.readAt;
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
const { data: nData, fetchMore } = useNotificationsQuery({
|
||||
variables: { limit: 5, filter },
|
||||
onCompleted: (d) => {
|
||||
setData((prev) => ({
|
||||
hasNextPage: d.notified.pageInfo.hasNextPage,
|
||||
cursor: d.notified.pageInfo.endCursor,
|
||||
nodes: [...prev.nodes, ...d.notified.notified],
|
||||
}));
|
||||
},
|
||||
});
|
||||
const { data: sData, loading } = useNotificationAddedSubscription({
|
||||
onSubscriptionData: (d) => {
|
||||
setData((n) => {
|
||||
if (d.subscriptionData.data) {
|
||||
return {
|
||||
...n,
|
||||
nodes: [d.subscriptionData.data.notificationAdded, ...n.nodes],
|
||||
};
|
||||
}
|
||||
return n;
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Popup title={null} tab={0} borders={false} padding={false}>
|
||||
<NotificationHeader>
|
||||
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
|
||||
</NotificationHeader>
|
||||
<ul>{children}</ul>
|
||||
<NotificationFooter>View All</NotificationFooter>
|
||||
<PopupContent>
|
||||
<NotificationHeader>
|
||||
<NotificationHeaderTitle>Notifications</NotificationHeaderTitle>
|
||||
</NotificationHeader>
|
||||
<NotificationTabs>
|
||||
{tabs.map((tab) => (
|
||||
<NotificationTab
|
||||
key={tab.key}
|
||||
onClick={() => {
|
||||
if (filter !== tab.key) {
|
||||
setData({ cursor: '', hasNextPage: false, nodes: [] });
|
||||
setFilter(tab.key);
|
||||
}
|
||||
}}
|
||||
active={tab.key === filter}
|
||||
>
|
||||
{tab.label}
|
||||
</NotificationTab>
|
||||
))}
|
||||
</NotificationTabs>
|
||||
<Notifications
|
||||
onScroll={({ currentTarget }) => {
|
||||
if (currentTarget.scrollTop + currentTarget.clientHeight >= currentTarget.scrollHeight) {
|
||||
if (data.hasNextPage) {
|
||||
console.log(`fetching more = ${data.cursor} - ${data.hasNextPage}`);
|
||||
fetchMore({
|
||||
variables: {
|
||||
limit: 5,
|
||||
filter,
|
||||
cursor: data.cursor,
|
||||
},
|
||||
updateQuery: (prev, { fetchMoreResult }) => {
|
||||
if (!fetchMoreResult) return prev;
|
||||
setData((d) => ({
|
||||
cursor: fetchMoreResult.notified.pageInfo.endCursor,
|
||||
hasNextPage: fetchMoreResult.notified.pageInfo.hasNextPage,
|
||||
nodes: [...d.nodes, ...fetchMoreResult.notified.notified],
|
||||
}));
|
||||
return {
|
||||
...prev,
|
||||
notified: {
|
||||
...prev.notified,
|
||||
pageInfo: {
|
||||
...fetchMoreResult.notified.pageInfo,
|
||||
},
|
||||
notified: [...prev.notified.notified, ...fetchMoreResult.notified.notified],
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{data.nodes.map((n) => (
|
||||
<Notification
|
||||
key={n.id}
|
||||
read={n.read}
|
||||
actionType={n.notification.actionType}
|
||||
data={n.notification.data}
|
||||
createdAt={n.notification.createdAt}
|
||||
causedBy={n.notification.causedBy}
|
||||
onToggleRead={() =>
|
||||
toggleRead({
|
||||
variables: { notifiedID: n.id },
|
||||
optimisticResponse: {
|
||||
__typename: 'Mutation',
|
||||
notificationToggleRead: {
|
||||
__typename: 'Notified',
|
||||
id: n.id,
|
||||
read: !n.read,
|
||||
readAt: new Date().toUTCString(),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</Notifications>
|
||||
</PopupContent>
|
||||
</Popup>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,6 +4,7 @@ import { mixin } from 'shared/utils/styles';
|
||||
import Button from 'shared/components/Button';
|
||||
import TaskAssignee from 'shared/components/TaskAssignee';
|
||||
import theme from 'App/ThemeStyles';
|
||||
import { Checkmark } from 'shared/icons';
|
||||
|
||||
export const Container = styled.div`
|
||||
display: flex;
|
||||
@@ -309,6 +310,7 @@ export const ActionButton = styled(Button)`
|
||||
text-align: left;
|
||||
transition: transform 0.2s ease;
|
||||
& span {
|
||||
position: unset;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
&:hover {
|
||||
@@ -717,3 +719,8 @@ export const TaskDetailsEditor = styled(TextareaAutosize)`
|
||||
outline: none;
|
||||
border: none;
|
||||
`;
|
||||
|
||||
export const WatchedCheckmark = styled(Checkmark)`
|
||||
position: absolute;
|
||||
right: 16px;
|
||||
`;
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
CheckSquareOutline,
|
||||
At,
|
||||
Smile,
|
||||
Eye,
|
||||
} from 'shared/icons';
|
||||
import { toArray } from 'react-emoji-render';
|
||||
import DOMPurify from 'dompurify';
|
||||
@@ -80,6 +81,7 @@ import {
|
||||
ActivityItemHeaderTitleName,
|
||||
ActivityItemComment,
|
||||
TabBarButton,
|
||||
WatchedCheckmark,
|
||||
} from './Styles';
|
||||
import Checklist, { ChecklistItem, ChecklistItems } from '../Checklist';
|
||||
import onDragEnd from './onDragEnd';
|
||||
@@ -237,6 +239,7 @@ type TaskDetailsProps = {
|
||||
onToggleChecklistItem: (itemID: string, complete: boolean) => void;
|
||||
onOpenAddMemberPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||
onOpenAddLabelPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||
onToggleTaskWatch: (task: Task, watched: boolean) => void;
|
||||
onOpenDueDatePopop: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||
onOpenAddChecklistPopup: (task: Task, $targetRef: React.RefObject<HTMLElement>) => void;
|
||||
onCreateComment: (task: Task, message: string) => void;
|
||||
@@ -258,6 +261,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
task,
|
||||
editableComment = null,
|
||||
onDeleteChecklist,
|
||||
onToggleTaskWatch,
|
||||
onTaskNameChange,
|
||||
onCommentShowActions,
|
||||
onOpenAddChecklistPopup,
|
||||
@@ -328,6 +332,7 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
const saveDescription = () => {
|
||||
onTaskDescriptionChange(task, taskDescriptionRef.current);
|
||||
};
|
||||
console.log(task.watched);
|
||||
return (
|
||||
<Container>
|
||||
<LeftSidebar>
|
||||
@@ -418,6 +423,14 @@ const TaskDetails: React.FC<TaskDetailsProps> = ({
|
||||
Checklist
|
||||
</ActionButton>
|
||||
<ActionButton>Cover</ActionButton>
|
||||
<ActionButton
|
||||
onClick={() => {
|
||||
onToggleTaskWatch(task, !task.watched);
|
||||
}}
|
||||
icon={<Eye width={12} height={12} />}
|
||||
>
|
||||
Watch {task.watched && <WatchedCheckmark width={18} height={18} />}
|
||||
</ActionButton>
|
||||
</ExtraActionsSection>
|
||||
)}
|
||||
</LeftSidebarContent>
|
||||
|
||||
@@ -6,11 +6,11 @@ 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};
|
||||
z-index: ${(props) => props.zIndex};
|
||||
position: relative;
|
||||
|
||||
box-shadow: 0 0 0 2px ${props => props.theme.colors.bg.primary},
|
||||
inset 0 0 0 1px ${props => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
|
||||
box-shadow: 0 0 0 2px ${(props) => props.theme.colors.bg.primary},
|
||||
inset 0 0 0 1px ${(props) => mixin.rgba(props.theme.colors.bg.primary, 0.07)};
|
||||
`;
|
||||
|
||||
export const NavbarWrapper = styled.div`
|
||||
@@ -27,9 +27,9 @@ export const NavbarHeader = styled.header`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background: ${props => props.theme.colors.bg.primary};
|
||||
background: ${(props) => props.theme.colors.bg.primary};
|
||||
box-shadow: 0 4px 20px 0 rgba(0, 0, 0, 0.05);
|
||||
border-bottom: 1px solid ${props => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
||||
border-bottom: 1px solid ${(props) => mixin.rgba(props.theme.colors.alternate, 0.65)};
|
||||
`;
|
||||
export const Breadcrumbs = styled.div`
|
||||
color: rgb(94, 108, 132);
|
||||
@@ -59,7 +59,7 @@ export const ProjectSwitchInner = styled.div`
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
background-color: ${props => props.theme.colors.primary};
|
||||
background-color: ${(props) => props.theme.colors.primary};
|
||||
`;
|
||||
|
||||
export const ProjectSwitch = styled.div`
|
||||
@@ -109,10 +109,27 @@ export const NavbarLink = styled(Link)`
|
||||
cursor: pointer;
|
||||
`;
|
||||
|
||||
export const NotificationCount = styled.div`
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
background: #7367f0;
|
||||
border-radius: 50%;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 3px solid rgb(16, 22, 58);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
`;
|
||||
|
||||
export const IconContainerWrapper = styled.div<{ disabled?: boolean }>`
|
||||
margin-right: 20px;
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
${props =>
|
||||
${(props) =>
|
||||
props.disabled &&
|
||||
css`
|
||||
opacity: 0.5;
|
||||
@@ -142,14 +159,14 @@ export const ProfileIcon = styled.div<{
|
||||
justify-content: center;
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
background: ${props => (props.backgroundURL ? `url(${props.backgroundURL})` : props.bgColor)};
|
||||
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;'}
|
||||
${(props) => !props.nameOnly && 'padding-top: 9px;'}
|
||||
margin-left: -6px;
|
||||
align-items: center;
|
||||
max-width: 100%;
|
||||
@@ -167,7 +184,7 @@ export const ProjectTabs = styled.div`
|
||||
|
||||
export const ProjectTab = styled(NavLink)`
|
||||
font-size: 80%;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-size: 15px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
@@ -184,22 +201,22 @@ export const ProjectTab = styled(NavLink)`
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 -2px ${props => props.theme.colors.text.secondary};
|
||||
color: ${props => props.theme.colors.text.secondary};
|
||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.text.secondary};
|
||||
color: ${(props) => props.theme.colors.text.secondary};
|
||||
}
|
||||
|
||||
&.active {
|
||||
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
|
||||
color: ${(props) => props.theme.colors.secondary};
|
||||
}
|
||||
&.active:hover {
|
||||
box-shadow: inset 0 -2px ${props => props.theme.colors.secondary};
|
||||
color: ${props => props.theme.colors.secondary};
|
||||
box-shadow: inset 0 -2px ${(props) => props.theme.colors.secondary};
|
||||
color: ${(props) => props.theme.colors.secondary};
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProjectName = styled.h1`
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-weight: 600;
|
||||
font-size: 20px;
|
||||
padding: 3px 10px 3px 8px;
|
||||
@@ -241,7 +258,7 @@ export const ProjectNameTextarea = styled.input`
|
||||
font-size: 20px;
|
||||
padding: 3px 10px 3px 8px;
|
||||
&:focus {
|
||||
box-shadow: ${props => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||
box-shadow: ${(props) => props.theme.colors.primary} 0px 0px 0px 1px;
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -259,7 +276,7 @@ export const ProjectSwitcher = styled.button`
|
||||
color: #c2c6dc;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: ${props => props.theme.colors.primary};
|
||||
background: ${(props) => props.theme.colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -283,7 +300,7 @@ export const ProjectSettingsButton = styled.button`
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background: ${props => props.theme.colors.primary};
|
||||
background: ${(props) => props.theme.colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -309,7 +326,7 @@ export const SignIn = styled(Button)`
|
||||
|
||||
export const NavSeparator = styled.div`
|
||||
width: 1px;
|
||||
background: ${props => props.theme.colors.border};
|
||||
background: ${(props) => props.theme.colors.border};
|
||||
height: 34px;
|
||||
margin: 0 20px;
|
||||
`;
|
||||
@@ -326,11 +343,11 @@ export const LogoContainer = styled(Link)`
|
||||
|
||||
export const TaskcafeTitle = styled.h2`
|
||||
margin-left: 5px;
|
||||
color: ${props => props.theme.colors.text.primary};
|
||||
color: ${(props) => props.theme.colors.text.primary};
|
||||
font-size: 20px;
|
||||
`;
|
||||
|
||||
export const TaskcafeLogo = styled(Taskcafe)`
|
||||
fill: ${props => props.theme.colors.text.primary};
|
||||
stroke: ${props => props.theme.colors.text.primary};
|
||||
fill: ${(props) => props.theme.colors.text.primary};
|
||||
stroke: ${(props) => props.theme.colors.text.primary};
|
||||
`;
|
||||
|
||||
@@ -36,6 +36,7 @@ import {
|
||||
ProjectMember,
|
||||
ProjectMembers,
|
||||
ProjectSwitchInner,
|
||||
NotificationCount,
|
||||
} from './Styles';
|
||||
|
||||
type IconContainerProps = {
|
||||
@@ -185,6 +186,7 @@ type NavBarProps = {
|
||||
projectMembers?: Array<TaskUser> | null;
|
||||
projectInvitedMembers?: Array<InvitedUser> | null;
|
||||
|
||||
hasUnread: boolean;
|
||||
onRemoveFromBoard?: (userID: string) => void;
|
||||
onMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, memberID: string) => void;
|
||||
onInvitedMemberProfile?: ($targetRef: React.RefObject<HTMLElement>, email: string) => void;
|
||||
@@ -203,6 +205,7 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
onOpenProjectFinder,
|
||||
onFavorite,
|
||||
onSetTab,
|
||||
hasUnread,
|
||||
projectInvitedMembers,
|
||||
onChangeRole,
|
||||
name,
|
||||
@@ -330,8 +333,9 @@ const NavBar: React.FC<NavBarProps> = ({
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<ListUnordered width={20} height={20} />
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={onNotificationClick}>
|
||||
<IconContainer onClick={onNotificationClick}>
|
||||
<Bell color="#c2c6dc" size={20} />
|
||||
{hasUnread && <NotificationCount />}
|
||||
</IconContainer>
|
||||
<IconContainer disabled onClick={NOOP}>
|
||||
<BarChart width={20} height={20} />
|
||||
|
||||
Reference in New Issue
Block a user