Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: boards page #136

Merged
merged 3 commits into from
Apr 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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",
},
});
nunocaseiro marked this conversation as resolved.
Show resolved Hide resolved

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;
201 changes: 201 additions & 0 deletions frontend/components/Boards/MyBoards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
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);
return;
}
const boardsOfDay = boardsOfTeam.get(date);
if (boardsOfDay) {
boardsOfDay.push(board);
return;
}
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>
);
})}

{isLoading && (
<Flex css={{ width: "100%", "& svg": { color: "black" } }} justify="center">
<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