Skip to content

Commit

Permalink
feat: boards page
Browse files Browse the repository at this point in the history
  • Loading branch information
nunocaseiro committed Apr 22, 2022
1 parent bbd4a2f commit 36546a1
Show file tree
Hide file tree
Showing 20 changed files with 607 additions and 260 deletions.
3 changes: 2 additions & 1 deletion frontend/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"import/no-cycle": "off",
"no-plusplus": "off",
"no-useless-escape": "off",
"no-restricted-exports": "off"
"no-restricted-exports": "off",
"react/require-default-props": "off"
}
}
54 changes: 54 additions & 0 deletions frontend/components/Boards/Filters/FilterBoards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { Dispatch, SetStateAction } from "react";
import { styled } from "../../../stitches.config";
import Button from "../../Primitives/Button";
import Flex from "../../Primitives/Flex";
import FilterSelect from "./FilterSelect";

export interface OptionType {
value: string;
label: string;
}

const StyledButton = styled(Button, {
border: "1px solid $colors$primary100",
borderRadius: "0px",
height: "$36 !important",
backgroundColor: "$background !important",
color: "$primary300 !important",
fontSize: "$14 !important",
lineHeight: "$20 !important",
fontWeight: "$medium !important",
"&[data-active='true']": {
borderColor: "$primary800",
color: "$primary800 !important",
},
"&:active": {
boxShadow: "none !important",
},
});

interface FilterBoardsProps {
setFilter: Dispatch<SetStateAction<string>>;
filter: string;
teamNames: OptionType[];
}

const FilterBoards: React.FC<FilterBoardsProps> = ({ setFilter, filter, teamNames }) => {
return (
<Flex justify="end" css={{ zIndex: "10" }}>
<StyledButton
css={{ borderRadius: "12px 0 0 12px" }}
data-active={filter === "all"}
onClick={() => setFilter("all")}
>
All
</StyledButton>
<StyledButton data-active={filter === "personal"} onClick={() => setFilter("personal")}>
Personal
</StyledButton>
<FilterSelect filter={filter} options={teamNames} setFilter={setFilter} />
</Flex>
);
};

export default FilterBoards;
37 changes: 37 additions & 0 deletions frontend/components/Boards/Filters/FilterSelect.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { Dispatch, SetStateAction } from "react";
import Select from "react-select";
import { styled } from "../../../stitches.config";

const StyledSelect = styled(Select, {});

interface OptionType {
value: string;
label: string;
}

interface FilterSelectProps {
options: OptionType[];
setFilter: Dispatch<SetStateAction<string>>;
filter: string;
}

const FilterSelect: React.FC<FilterSelectProps> = ({ filter, options, setFilter }) => {
const isSelected = filter !== "all" && filter !== "personal";
return (
<StyledSelect
options={options}
className="react-select-container"
classNamePrefix="react-select"
value={
!isSelected
? { label: "Select", value: "" }
: options.find((option) => option.value === filter)
}
onChange={(selectedOption) => {
setFilter((selectedOption as OptionType)?.value);
}}
/>
);
};

export default FilterSelect;
198 changes: 198 additions & 0 deletions frontend/components/Boards/MyBoards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import React, { useMemo, useRef, useState } from "react";
import { useInfiniteQuery } from "react-query";
import { useSetRecoilState } from "recoil";
import { TailSpin } from "react-loader-spinner";
import { getBoardsRequest } from "../../api/boardService";
import { toastState } from "../../store/toast/atom/toast.atom";
import BoardType from "../../types/board/board";
import { ToastStateEnum } from "../../utils/enums/toast-types";
import CardBody from "../CardBoard/CardBody/CardBody";
import Flex from "../Primitives/Flex";
import Text from "../Primitives/Text";
import TeamHeader from "./TeamHeader";
import { Team } from "../../types/team/team";
import FilterBoards from "./Filters/FilterBoards";

interface MyBoardsProps {
userId: string;
isSuperAdmin: boolean;
}

const MyBoards = React.memo<MyBoardsProps>(({ userId, isSuperAdmin }) => {
const setToastState = useSetRecoilState(toastState);
const [filter, setFilter] = useState("all");
const scrollRef = useRef<HTMLDivElement>(null);

const fetchBoards = useInfiniteQuery(
"boards",
({ pageParam = 0 }) => getBoardsRequest(pageParam),
{
enabled: true,
refetchOnWindowFocus: false,
getNextPageParam: (lastPage) => {
const { hasNextPage, page } = lastPage;
if (hasNextPage) return page + 1;
return undefined;
},
onError: () => {
setToastState({
open: true,
content: "Error getting the boards",
type: ToastStateEnum.ERROR,
});
},
}
);

const { data, isLoading } = fetchBoards;

const currentDate = new Date().toDateString();

const dataByTeamAndDate = useMemo(() => {
const teams = new Map<string, Team>();
const boardsTeamAndDate = new Map<string, Map<string, BoardType[]>>();

data?.pages.forEach((page) => {
page.boards?.forEach((board) => {
const boardsOfTeam = boardsTeamAndDate.get(`${board.team?._id ?? `personal`}`);
const date = new Date(board.updatedAt).toDateString();
if (!boardsOfTeam) {
boardsTeamAndDate.set(`${board.team?._id ?? `personal`}`, new Map([[date, [board]]]));
if (board.team) teams.set(`${board.team?._id}`, board.team);
} else {
const boardsOfDay = boardsOfTeam.get(date);
if (boardsOfDay) {
boardsOfDay.push(board);
} else {
boardsOfTeam.set(date, [board]);
}
}
});
});
return { boardsTeamAndDate, teams };
}, [data?.pages]);

const onScroll = () => {
if (scrollRef.current) {
const { scrollTop, scrollHeight, clientHeight } = scrollRef.current;
if (scrollTop + clientHeight + 2 >= scrollHeight && fetchBoards.hasNextPage) {
fetchBoards.fetchNextPage();
}
}
};

const teamNames = Array.from(dataByTeamAndDate.teams.values()).map((team) => {
return { value: team._id, label: team.name };
});

return (
<Flex
ref={scrollRef}
onScroll={onScroll}
css={{ mt: "$24", height: "100vh", overflow: "scroll", pr: "$20" }}
justify="start"
direction="column"
>
<FilterBoards setFilter={setFilter} teamNames={teamNames} filter={filter} />
{Array.from(dataByTeamAndDate.boardsTeamAndDate).map(([teamId, boardsOfTeam]) => {
const { users } = Array.from(boardsOfTeam)[0][1][0];
if (filter !== "all" && teamId !== filter) return null;
return (
<Flex direction="column" key={teamId} css={{ mb: "$24" }}>
<Flex
direction="column"
css={{
position: "sticky",
zIndex: "5",
top: "-0.4px",
backgroundColor: "$background",
}}
>
<TeamHeader
team={dataByTeamAndDate.teams.get(teamId)}
users={users}
userId={userId}
/>
</Flex>
<Flex direction="column" gap="16" css={{ overflow: "scroll", zIndex: "1" }}>
{Array.from(boardsOfTeam).map(([date, boardsOfDay]) => {
const formatedDate = new Date(date).toLocaleDateString("en-US", {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric",
});
return (
<Flex direction="column" key={date}>
<Text
size="xs"
color="primary300"
css={{
position: "sticky",
zIndex: "5",
top: "-0.2px",
height: "$24",
backgroundColor: "$background",
}}
>
Last updated -{" "}
{date === currentDate ? `Today, ${formatedDate}` : formatedDate}
</Text>
{/* to be used on the full version -> <Flex justify="end" css={{ width: "100%" }}>
<Flex
css={{
position: "relative",
zIndex: "30",
"& svg": { size: "$16" },
right: 0,
top: "-22px",
}}
gap="8"
>
<PlusIcon size="16" />
<Text
heading="6"
css={{
width: "fit-content",
display: "flex",
alignItems: "center",
"@hover": {
"&:hover": {
cursor: "pointer",
},
},
}}
>
{!Array.from(dataByTeamAndDate.teams.keys()).includes(teamId)
? "Add new personal board"
: "Add new team board"}
</Text>
</Flex>
</Flex> */}
<Flex gap="8" direction="column">
{boardsOfDay.map((board: BoardType) => (
<CardBody
key={board._id}
userId={userId}
board={board}
isDashboard={false}
dividedBoardsCount={board.dividedBoards.length}
isSAdmin={isSuperAdmin}
/>
))}
</Flex>
</Flex>
);
})}
</Flex>
</Flex>
);
})}
<Flex css={{ width: "100%", "& svg": { color: "black" } }} justify="center">
{isLoading && <TailSpin color="#060D16" height={60} width={60} />}
</Flex>
</Flex>
);
});

export default MyBoards;
59 changes: 59 additions & 0 deletions frontend/components/Boards/TeamHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { BoardUser } from "../../types/board/board.user";
import { Team } from "../../types/team/team";
import CardAvatars from "../CardBoard/CardAvatars";
import Flex from "../Primitives/Flex";
import Separator from "../Primitives/Separator";
import Text from "../Primitives/Text";

interface TeamHeaderProps {
team?: Team;
userId: string;
users?: BoardUser[];
}

const TeamHeader: React.FC<TeamHeaderProps> = ({ team, userId, users }) => {
const hasTeam = !!team;
return (
<Flex align="center" css={{ mb: "$16" }} justify="between">
<Flex align="center">
<Text heading="5">{hasTeam ? team.name : "My boards"}</Text>
{hasTeam && (
<Flex align="center" css={{ ml: "$24" }} gap="12">
<Flex gap="8" align="center">
<Text size="sm" color="primary300">
Members
</Text>
<CardAvatars
listUsers={team.users}
responsible={false}
teamAdmins={false}
userId={userId}
/>
</Flex>
<Separator
orientation="vertical"
css={{ backgroundColor: "$primary300", height: "$12 !important" }}
/>
<Text size="sm" color="primary300">
Team admin
</Text>
<CardAvatars listUsers={team.users} responsible={false} teamAdmins userId={userId} />
</Flex>
)}
{!hasTeam && users && (
<Flex css={{ ml: "$12" }}>
<CardAvatars
listUsers={users}
responsible={false}
teamAdmins={false}
userId={userId}
myBoards
/>
</Flex>
)}
</Flex>
</Flex>
);
};

export default TeamHeader;
Loading

0 comments on commit 36546a1

Please sign in to comment.