diff --git a/src/components/Button.tsx b/src/components/Button.tsx index 521c1b40..51d362fb 100644 --- a/src/components/Button.tsx +++ b/src/components/Button.tsx @@ -3,10 +3,12 @@ import { twMerge } from "tailwind-merge"; interface ButtonProps extends React.ButtonHTMLAttributes { variant?: "primary" | "secondary" | "danger"; isLoading?: boolean; + icon?: React.ReactNode; } const Button = ({ children, + icon, isLoading, variant = "primary", ...props @@ -14,7 +16,7 @@ const Button = ({ return ( ); diff --git a/src/components/CheckboxDropdown.tsx b/src/components/CheckboxDropdown.tsx index fc792a78..2557e4d1 100644 --- a/src/components/CheckboxDropdown.tsx +++ b/src/components/CheckboxDropdown.tsx @@ -1,27 +1,50 @@ -import { Fragment } from "react"; +import { Fragment, useState } from "react"; import { Menu, Transition } from "@headlessui/react"; +import { twMerge } from "tailwind-merge"; + +interface Item { + key: string; + value: string; + selected: boolean; + leftIcon?: React.ReactNode; +} + +interface Group { + key: string; + label: string; + icon: React.ReactNode; + items: Item[]; +} interface CheckboxDropdownProps { children: React.ReactNode; - items: { - key: string; - value: string | null; - selected: boolean; - }[]; - handleSelect: (item: { key: string }) => void; + items?: Item[]; + groups?: Group[]; + menuSpacing?: "sm" | "md" | "lg"; + handleSelect: (groupKey: string | null, item: { key: string }) => void; } export default function CheckboxDropdown({ children, items, + groups, + menuSpacing = "sm", handleSelect, }: CheckboxDropdownProps) { + const [selectedGroup, setSelectedGroup] = useState(null); + + const menuSpacingClass = { + sm: "top-[26px]", + md: "top-[32px]", + lg: "top-[38px]", + }; + return ( - <> - + + <> {children} @@ -34,41 +57,110 @@ export default function CheckboxDropdown({ leave="transition ease-in duration-75" leaveFrom="transform opacity-100 scale-100" leaveTo="transform opacity-0 scale-95" + afterLeave={() => setSelectedGroup(null)} > - +
- {items?.map((item) => ( - -
{ - e.preventDefault(); - handleSelect({ key: item.key }); - }} - > - event.stopPropagation()} - onChange={() => handleSelect({ key: item.key })} - checked={item.selected} - /> - -
-
- ))} + {!selectedGroup ? ( + <> + {items?.map((item) => ( + +
{ + e.preventDefault(); + handleSelect(null, { key: item.key }); + }} + > + event.stopPropagation()} + onChange={() => handleSelect(null, { key: item.key })} + checked={item.selected} + /> + {item.leftIcon && ( + + {item.leftIcon} + + )} + +
+
+ ))} + {groups?.map((group) => ( + +
{ + e.preventDefault(); + setSelectedGroup(group.key); + }} + > + {group.icon} + + {group.label} + +
+
+ ))} + + ) : ( + <> + {groups + ?.find((g) => g.key === selectedGroup) + ?.items.map((item) => ( + +
{ + e.preventDefault(); + handleSelect(selectedGroup, { key: item.key }); + }} + > + event.stopPropagation()} + onChange={() => + handleSelect(selectedGroup, { key: item.key }) + } + checked={item.selected} + /> + {item.leftIcon && ( + + {item.leftIcon} + + )} + +
+
+ ))} + + )}
-
- + +
); } diff --git a/src/providers/board.tsx b/src/providers/board.tsx index 66c862e2..226afd35 100644 --- a/src/providers/board.tsx +++ b/src/providers/board.tsx @@ -56,6 +56,10 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({ try { const data = await utils.board.byId.fetch({ boardPublicId: boardData.publicId, + filters: { + members: [], + labels: [], + }, }); if (data) setBoardData(data); } catch (e) { diff --git a/src/server/api/routers/board.ts b/src/server/api/routers/board.ts index edc232fd..08afa350 100644 --- a/src/server/api/routers/board.ts +++ b/src/server/api/routers/board.ts @@ -28,9 +28,23 @@ export const boardRouter = createTRPCRouter({ return result; }), byId: protectedProcedure - .input(z.object({ boardPublicId: z.string().min(12) })) + .input( + z.object({ + boardPublicId: z.string().min(12), + filters: z.object({ + members: z.array(z.string().min(12)), + labels: z.array(z.string().min(12)), + }), + }), + ) .query(async ({ ctx, input }) => { - const result = await boardRepo.getByPublicId(ctx.db, input.boardPublicId); + const result = await boardRepo.getByPublicId( + ctx.db, + input.boardPublicId, + input.filters, + ); + + console.log(result); return result; }), diff --git a/src/server/db/repository/board.repo.ts b/src/server/db/repository/board.repo.ts index cd754fd8..7e17864c 100644 --- a/src/server/db/repository/board.repo.ts +++ b/src/server/db/repository/board.repo.ts @@ -18,8 +18,12 @@ export const getAllByWorkspaceId = async ( export const getByPublicId = async ( db: SupabaseClient, boardPublicId: string, + filters: { + members: string[]; + labels: string[]; + }, ) => { - const { data } = await db + let query = db .from("board") .select( ` @@ -50,12 +54,12 @@ export const getByPublicId = async ( description, listId, index, - labels:label ( + labels:label${filters.labels.length > 0 ? "!inner" : ""} ( publicId, name, colourCode ), - members:workspace_members ( + members:workspace_members${filters.members.length > 0 ? "!inner" : ""} ( publicId, user ( name @@ -68,12 +72,24 @@ export const getByPublicId = async ( .eq("publicId", boardPublicId) .is("deletedAt", null) .is("lists.deletedAt", null) - .is("lists.cards.deletedAt", null) + .is("lists.cards.deletedAt", null); + + if (filters.labels.length > 0) { + query = query.in("lists.cards.labels.publicId", filters.labels); + } + + if (filters.members.length > 0) { + query = query.in("lists.cards.members.publicId", filters.members); + } + + const { data, error } = await query .order("index", { foreignTable: "list", ascending: true }) .order("index", { foreignTable: "list.card", ascending: true }) .limit(1) .single(); + console.log(error); + return data; }; diff --git a/src/server/db/repository/workspace.repo.ts b/src/server/db/repository/workspace.repo.ts index 0af00eca..f99dff7f 100644 --- a/src/server/db/repository/workspace.repo.ts +++ b/src/server/db/repository/workspace.repo.ts @@ -43,7 +43,7 @@ export const update = async ( workspacePublicId: string, name: string, ) => { - const { data, error } = await db + const { data } = await db .from("workspace") .update({ name }) .eq("publicId", workspacePublicId) diff --git a/src/views/board/components/DeleteListConfirmation.tsx b/src/views/board/components/DeleteListConfirmation.tsx index a89bcde3..7fcb4021 100644 --- a/src/views/board/components/DeleteListConfirmation.tsx +++ b/src/views/board/components/DeleteListConfirmation.tsx @@ -18,7 +18,13 @@ export function DeleteListConfirmation({ const refetchBoard = async () => { if (boardData?.publicId) { try { - await utils.board.byId.refetch({ boardPublicId: boardData.publicId }); + await utils.board.byId.refetch({ + boardPublicId: boardData.publicId, + filters: { + members: [], + labels: [], + }, + }); } catch (e) { console.error(e); } diff --git a/src/views/board/components/Filters.tsx b/src/views/board/components/Filters.tsx new file mode 100644 index 00000000..5d12d4d6 --- /dev/null +++ b/src/views/board/components/Filters.tsx @@ -0,0 +1,105 @@ +import { IoFilterOutline } from "react-icons/io5"; +import { HiOutlineUserCircle, HiOutlineTag } from "react-icons/hi2"; +import { useRouter } from "next/router"; +import Button from "~/components/Button"; +import CheckboxDropdown from "~/components/CheckboxDropdown"; + +import { useBoard } from "~/providers/board"; + +const LabelIcon = ({ colourCode }: { colourCode: string | null }) => ( + +); + +const Avatar = ({ name }: { name: string }) => ( + + + {name + ?.split(" ") + .map((namePart) => namePart.charAt(0).toUpperCase()) + .join("")} + + +); + +const Filters = () => { + const { boardData } = useBoard(); + const router = useRouter(); + + const formattedMembers = + boardData?.workspace?.members?.map((member) => ({ + key: member.publicId, + value: member.user?.name ?? "", + selected: !!router.query.members?.includes(member.publicId), + leftIcon: , + })) ?? []; + + const formattedLabels = + boardData?.labels.map((label) => ({ + key: label.publicId, + value: label.name, + selected: !!router.query.labels?.includes(label.publicId), + leftIcon: , + })) ?? []; + + const groups = [ + { + key: "members", + label: "Members", + icon: , + items: formattedMembers, + }, + { + key: "labels", + label: "Labels", + icon: , + items: formattedLabels, + }, + ]; + + const handleSelect = async ( + groupKey: string | null, + item: { key: string }, + ) => { + if (groupKey === null) return; + const currentQuery = router.query[groupKey] ?? []; + const formattedCurrentQuery = Array.isArray(currentQuery) + ? currentQuery + : [currentQuery]; + + const updatedQuery = formattedCurrentQuery.includes(item.key) + ? formattedCurrentQuery.filter((key) => key !== item.key) + : [...formattedCurrentQuery, item.key]; + + try { + await router.push({ + pathname: router.pathname, + query: { ...router.query, [groupKey]: updatedQuery }, + }); + } catch (error) { + console.error(error); + } + }; + + return ( +
+ + + +
+ ); +}; + +export default Filters; diff --git a/src/views/board/components/List.tsx b/src/views/board/components/List.tsx index eff9aaa5..c0352f99 100644 --- a/src/views/board/components/List.tsx +++ b/src/views/board/components/List.tsx @@ -64,7 +64,7 @@ export default function List({ ref={provided.innerRef} {...provided.draggableProps} {...provided.dragHandleProps} - className="dark-text-dark-1000 mr-5 h-fit min-w-[18rem] max-w-[18rem] rounded-md border border-light-400 bg-light-300 py-2 pl-2 pr-1 text-neutral-900 dark:border-dark-300 dark:bg-dark-100" + className="dark-text-dark-1000 z-0 mr-5 h-fit min-w-[18rem] max-w-[18rem] rounded-md border border-light-400 bg-light-300 py-2 pl-2 pr-1 text-neutral-900 dark:border-dark-300 dark:bg-dark-100" >
- handleSelectList(list.key) - } + handleSelect={(_groupKey, item) => handleSelectList(item.key)} >
{selectedList?.value} @@ -190,9 +188,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
- handleSelectMembers(list.key) - } + handleSelect={(_groupKey, item) => handleSelectMembers(item.key)} >
{!memberPublicIds.length ? ( @@ -228,9 +224,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
- handleSelectLabels(list.key) - } + handleSelect={(_groupKey, item) => handleSelectLabels(item.key)} >
{!labelPublicIds.length ? ( diff --git a/src/views/board/index.tsx b/src/views/board/index.tsx index 9a36fe7f..cb39d91d 100644 --- a/src/views/board/index.tsx +++ b/src/views/board/index.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from "react"; import Link from "next/link"; +import { useRouter } from "next/router"; import { useParams } from "next/navigation"; import { HiOutlinePlusSmall } from "react-icons/hi2"; import { @@ -24,13 +25,22 @@ import List from "./components/List"; import { NewWorkspaceForm } from "~/components/NewWorkspaceForm"; import { NewCardForm } from "./components/NewCardForm"; import { NewListForm } from "./components/NewListForm"; +import Filters from "./components/Filters"; import { type UpdateBoardInput } from "~/types/router.types"; type PublicListId = string; +const formatToArray = (value: string | string[] | undefined): string[] => { + if (Array.isArray(value)) { + return value.filter((item) => item !== undefined); + } + return value ? [value] : []; +}; + export default function BoardPage() { const params = useParams(); + const router = useRouter(); const { boardData, setBoardData, updateCard, updateList } = useBoard(); const { openModal, modalContentType } = useModal(); const [selectedPublicListId, setSelectedPublicListId] = @@ -55,7 +65,13 @@ export default function BoardPage() { }; const { data, isSuccess, isLoading } = api.board.byId.useQuery( - { boardPublicId: boardId ?? "" }, + { + boardPublicId: boardId ?? "", + filters: { + members: formatToArray(router.query.members), + labels: formatToArray(router.query.labels), + }, + }, { enabled: !!boardId, }, @@ -150,7 +166,8 @@ export default function BoardPage() { )} -
+
+
-
+
{isLoading ? (
diff --git a/src/views/settings/index.tsx b/src/views/settings/index.tsx index 9d57919f..6087b70f 100644 --- a/src/views/settings/index.tsx +++ b/src/views/settings/index.tsx @@ -1,5 +1,3 @@ -import { useForm } from "react-hook-form"; - import Modal from "~/components/modal"; import Button from "~/components/Button";