feat: card filters
This commit is contained in:
@@ -3,10 +3,12 @@ import { twMerge } from "tailwind-merge";
|
|||||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: "primary" | "secondary" | "danger";
|
variant?: "primary" | "secondary" | "danger";
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
|
icon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Button = ({
|
const Button = ({
|
||||||
children,
|
children,
|
||||||
|
icon,
|
||||||
isLoading,
|
isLoading,
|
||||||
variant = "primary",
|
variant = "primary",
|
||||||
...props
|
...props
|
||||||
@@ -14,7 +16,7 @@ const Button = ({
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={twMerge(
|
className={twMerge(
|
||||||
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none",
|
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-semibold text-light-1000 shadow-sm focus-visible:outline-none",
|
||||||
variant === "primary" &&
|
variant === "primary" &&
|
||||||
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
|
"bg-light-1000 dark:bg-dark-1000 dark:text-dark-50",
|
||||||
variant === "secondary" &&
|
variant === "secondary" &&
|
||||||
@@ -51,7 +53,15 @@ const Button = ({
|
|||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
<span className={isLoading ? "invisible" : "visible"}>{children}</span>
|
<div
|
||||||
|
className={twMerge(
|
||||||
|
"flex items-center",
|
||||||
|
isLoading ? "invisible" : "visible",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{icon && <span className="mr-2">{icon}</span>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,27 +1,50 @@
|
|||||||
import { Fragment } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/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 {
|
interface CheckboxDropdownProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
items: {
|
items?: Item[];
|
||||||
key: string;
|
groups?: Group[];
|
||||||
value: string | null;
|
menuSpacing?: "sm" | "md" | "lg";
|
||||||
selected: boolean;
|
handleSelect: (groupKey: string | null, item: { key: string }) => void;
|
||||||
}[];
|
|
||||||
handleSelect: (item: { key: string }) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CheckboxDropdown({
|
export default function CheckboxDropdown({
|
||||||
children,
|
children,
|
||||||
items,
|
items,
|
||||||
|
groups,
|
||||||
|
menuSpacing = "sm",
|
||||||
handleSelect,
|
handleSelect,
|
||||||
}: CheckboxDropdownProps) {
|
}: CheckboxDropdownProps) {
|
||||||
|
const [selectedGroup, setSelectedGroup] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const menuSpacingClass = {
|
||||||
|
sm: "top-[26px]",
|
||||||
|
md: "top-[32px]",
|
||||||
|
lg: "top-[38px]",
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<Menu
|
||||||
<Menu
|
as="div"
|
||||||
as="div"
|
className="relative flex w-full flex-wrap items-center text-left"
|
||||||
className="relative flex w-full flex-wrap items-center text-left"
|
>
|
||||||
>
|
<>
|
||||||
<Menu.Button className="focus-visible:outline-none">
|
<Menu.Button className="focus-visible:outline-none">
|
||||||
{children}
|
{children}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -34,41 +57,110 @@ export default function CheckboxDropdown({
|
|||||||
leave="transition ease-in duration-75"
|
leave="transition ease-in duration-75"
|
||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
|
afterLeave={() => setSelectedGroup(null)}
|
||||||
>
|
>
|
||||||
<Menu.Items className="absolute left-0 top-[26px] z-10 mt-2 w-56 origin-top-left rounded-md border-[1px] border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
<Menu.Items
|
||||||
|
className={twMerge(
|
||||||
|
"absolute left-0 z-50 mt-2 w-56 origin-top-left rounded-md border-[1px] border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200",
|
||||||
|
menuSpacingClass[menuSpacing],
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{items?.map((item) => (
|
{!selectedGroup ? (
|
||||||
<Menu.Item key={item.key}>
|
<>
|
||||||
<div
|
{items?.map((item) => (
|
||||||
key={item.key}
|
<Menu.Item key={item.key}>
|
||||||
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
<div
|
||||||
onClick={(e) => {
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
e.preventDefault();
|
onClick={(e) => {
|
||||||
handleSelect({ key: item.key });
|
e.preventDefault();
|
||||||
}}
|
handleSelect(null, { key: item.key });
|
||||||
>
|
}}
|
||||||
<input
|
>
|
||||||
id={item.key}
|
<input
|
||||||
name={item.key}
|
id={item.key}
|
||||||
type="checkbox"
|
name={item.key}
|
||||||
className="h-[14px] w-[14px] rounded bg-transparent"
|
type="checkbox"
|
||||||
onClick={(event) => event.stopPropagation()}
|
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||||
onChange={() => handleSelect({ key: item.key })}
|
onClick={(event) => event.stopPropagation()}
|
||||||
checked={item.selected}
|
onChange={() => handleSelect(null, { key: item.key })}
|
||||||
/>
|
checked={item.selected}
|
||||||
<label
|
/>
|
||||||
htmlFor={item.key}
|
{item.leftIcon && (
|
||||||
className="ml-3 text-sm text-dark-900"
|
<span className="ml-3 flex items-center">
|
||||||
>
|
{item.leftIcon}
|
||||||
{item.value}
|
</span>
|
||||||
</label>
|
)}
|
||||||
</div>
|
<label
|
||||||
</Menu.Item>
|
htmlFor={item.key}
|
||||||
))}
|
className="ml-3 text-[12px] text-dark-900"
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
{groups?.map((group) => (
|
||||||
|
<Menu.Item key={group.key}>
|
||||||
|
<div
|
||||||
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setSelectedGroup(group.key);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="mr-2 text-dark-900">{group.icon}</span>
|
||||||
|
<span className="pointer-events-none text-[12px] text-dark-900">
|
||||||
|
{group.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{groups
|
||||||
|
?.find((g) => g.key === selectedGroup)
|
||||||
|
?.items.map((item) => (
|
||||||
|
<Menu.Item key={item.key}>
|
||||||
|
<div
|
||||||
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSelect(selectedGroup, { key: item.key });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id={item.key}
|
||||||
|
name={item.key}
|
||||||
|
type="checkbox"
|
||||||
|
className="h-[14px] w-[14px] rounded bg-transparent"
|
||||||
|
onClick={(event) => event.stopPropagation()}
|
||||||
|
onChange={() =>
|
||||||
|
handleSelect(selectedGroup, { key: item.key })
|
||||||
|
}
|
||||||
|
checked={item.selected}
|
||||||
|
/>
|
||||||
|
{item.leftIcon && (
|
||||||
|
<span className="ml-3 flex items-center">
|
||||||
|
{item.leftIcon}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<label
|
||||||
|
htmlFor={item.key}
|
||||||
|
className="ml-3 text-[12px] text-dark-900"
|
||||||
|
>
|
||||||
|
{item.value}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</Menu.Item>
|
||||||
|
))}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</Menu.Items>
|
</Menu.Items>
|
||||||
</Transition>
|
</Transition>
|
||||||
</Menu>
|
</>
|
||||||
</>
|
</Menu>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,10 @@ export const BoardProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
try {
|
try {
|
||||||
const data = await utils.board.byId.fetch({
|
const data = await utils.board.byId.fetch({
|
||||||
boardPublicId: boardData.publicId,
|
boardPublicId: boardData.publicId,
|
||||||
|
filters: {
|
||||||
|
members: [],
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (data) setBoardData(data);
|
if (data) setBoardData(data);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -28,9 +28,23 @@ export const boardRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
byId: protectedProcedure
|
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 }) => {
|
.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;
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -18,8 +18,12 @@ export const getAllByWorkspaceId = async (
|
|||||||
export const getByPublicId = async (
|
export const getByPublicId = async (
|
||||||
db: SupabaseClient<Database>,
|
db: SupabaseClient<Database>,
|
||||||
boardPublicId: string,
|
boardPublicId: string,
|
||||||
|
filters: {
|
||||||
|
members: string[];
|
||||||
|
labels: string[];
|
||||||
|
},
|
||||||
) => {
|
) => {
|
||||||
const { data } = await db
|
let query = db
|
||||||
.from("board")
|
.from("board")
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
@@ -50,12 +54,12 @@ export const getByPublicId = async (
|
|||||||
description,
|
description,
|
||||||
listId,
|
listId,
|
||||||
index,
|
index,
|
||||||
labels:label (
|
labels:label${filters.labels.length > 0 ? "!inner" : ""} (
|
||||||
publicId,
|
publicId,
|
||||||
name,
|
name,
|
||||||
colourCode
|
colourCode
|
||||||
),
|
),
|
||||||
members:workspace_members (
|
members:workspace_members${filters.members.length > 0 ? "!inner" : ""} (
|
||||||
publicId,
|
publicId,
|
||||||
user (
|
user (
|
||||||
name
|
name
|
||||||
@@ -68,12 +72,24 @@ export const getByPublicId = async (
|
|||||||
.eq("publicId", boardPublicId)
|
.eq("publicId", boardPublicId)
|
||||||
.is("deletedAt", null)
|
.is("deletedAt", null)
|
||||||
.is("lists.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", ascending: true })
|
||||||
.order("index", { foreignTable: "list.card", ascending: true })
|
.order("index", { foreignTable: "list.card", ascending: true })
|
||||||
.limit(1)
|
.limit(1)
|
||||||
.single();
|
.single();
|
||||||
|
|
||||||
|
console.log(error);
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export const update = async (
|
|||||||
workspacePublicId: string,
|
workspacePublicId: string,
|
||||||
name: string,
|
name: string,
|
||||||
) => {
|
) => {
|
||||||
const { data, error } = await db
|
const { data } = await db
|
||||||
.from("workspace")
|
.from("workspace")
|
||||||
.update({ name })
|
.update({ name })
|
||||||
.eq("publicId", workspacePublicId)
|
.eq("publicId", workspacePublicId)
|
||||||
|
|||||||
@@ -18,7 +18,13 @@ export function DeleteListConfirmation({
|
|||||||
const refetchBoard = async () => {
|
const refetchBoard = async () => {
|
||||||
if (boardData?.publicId) {
|
if (boardData?.publicId) {
|
||||||
try {
|
try {
|
||||||
await utils.board.byId.refetch({ boardPublicId: boardData.publicId });
|
await utils.board.byId.refetch({
|
||||||
|
boardPublicId: boardData.publicId,
|
||||||
|
filters: {
|
||||||
|
members: [],
|
||||||
|
labels: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
}
|
}
|
||||||
|
|||||||
105
src/views/board/components/Filters.tsx
Normal file
105
src/views/board/components/Filters.tsx
Normal file
@@ -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 }) => (
|
||||||
|
<svg
|
||||||
|
fill={colourCode ?? "#3730a3"}
|
||||||
|
className="h-2 w-2"
|
||||||
|
viewBox="0 0 6 6"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<circle cx={3} cy={3} r={3} />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Avatar = ({ name }: { name: string }) => (
|
||||||
|
<span className="inline-flex h-4 w-4 items-center justify-center rounded-full bg-gray-400 ring-1 ring-light-200 dark:ring-dark-500">
|
||||||
|
<span className="text-[8px] font-medium leading-none text-white">
|
||||||
|
{name
|
||||||
|
?.split(" ")
|
||||||
|
.map((namePart) => namePart.charAt(0).toUpperCase())
|
||||||
|
.join("")}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
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: <Avatar name={member.user?.name ?? ""} />,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const formattedLabels =
|
||||||
|
boardData?.labels.map((label) => ({
|
||||||
|
key: label.publicId,
|
||||||
|
value: label.name,
|
||||||
|
selected: !!router.query.labels?.includes(label.publicId),
|
||||||
|
leftIcon: <LabelIcon colourCode={label.colourCode} />,
|
||||||
|
})) ?? [];
|
||||||
|
|
||||||
|
const groups = [
|
||||||
|
{
|
||||||
|
key: "members",
|
||||||
|
label: "Members",
|
||||||
|
icon: <HiOutlineUserCircle size={16} />,
|
||||||
|
items: formattedMembers,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: "labels",
|
||||||
|
label: "Labels",
|
||||||
|
icon: <HiOutlineTag size={16} />,
|
||||||
|
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 (
|
||||||
|
<div className="relative">
|
||||||
|
<CheckboxDropdown
|
||||||
|
groups={groups}
|
||||||
|
handleSelect={handleSelect}
|
||||||
|
menuSpacing="md"
|
||||||
|
>
|
||||||
|
<Button variant="secondary" icon={<IoFilterOutline />}>
|
||||||
|
Filter
|
||||||
|
</Button>
|
||||||
|
</CheckboxDropdown>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Filters;
|
||||||
@@ -64,7 +64,7 @@ export default function List({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...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"
|
||||||
>
|
>
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
<form
|
<form
|
||||||
|
|||||||
@@ -178,9 +178,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<CheckboxDropdown
|
<CheckboxDropdown
|
||||||
items={formattedLists}
|
items={formattedLists}
|
||||||
handleSelect={(list: { key: string }) =>
|
handleSelect={(_groupKey, item) => handleSelectList(item.key)}
|
||||||
handleSelectList(list.key)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||||
{selectedList?.value}
|
{selectedList?.value}
|
||||||
@@ -190,9 +188,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<CheckboxDropdown
|
<CheckboxDropdown
|
||||||
items={formattedMembers}
|
items={formattedMembers}
|
||||||
handleSelect={(list: { key: string }) =>
|
handleSelect={(_groupKey, item) => handleSelectMembers(item.key)}
|
||||||
handleSelectMembers(list.key)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||||
{!memberPublicIds.length ? (
|
{!memberPublicIds.length ? (
|
||||||
@@ -228,9 +224,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
<div className="w-fit">
|
<div className="w-fit">
|
||||||
<CheckboxDropdown
|
<CheckboxDropdown
|
||||||
items={formattedLabels}
|
items={formattedLabels}
|
||||||
handleSelect={(list: { key: string }) =>
|
handleSelect={(_groupKey, item) => handleSelectLabels(item.key)}
|
||||||
handleSelectLabels(list.key)
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
<div className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-600 bg-light-200 px-2 py-1 text-left text-xs text-light-800 hover:bg-light-300 dark:border-dark-600 dark:bg-dark-400 dark:text-dark-1000 dark:hover:bg-dark-500">
|
||||||
{!labelPublicIds.length ? (
|
{!labelPublicIds.length ? (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
import {
|
import {
|
||||||
@@ -24,13 +25,22 @@ import List from "./components/List";
|
|||||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import { NewCardForm } from "./components/NewCardForm";
|
import { NewCardForm } from "./components/NewCardForm";
|
||||||
import { NewListForm } from "./components/NewListForm";
|
import { NewListForm } from "./components/NewListForm";
|
||||||
|
import Filters from "./components/Filters";
|
||||||
|
|
||||||
import { type UpdateBoardInput } from "~/types/router.types";
|
import { type UpdateBoardInput } from "~/types/router.types";
|
||||||
|
|
||||||
type PublicListId = string;
|
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() {
|
export default function BoardPage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
const router = useRouter();
|
||||||
const { boardData, setBoardData, updateCard, updateList } = useBoard();
|
const { boardData, setBoardData, updateCard, updateList } = useBoard();
|
||||||
const { openModal, modalContentType } = useModal();
|
const { openModal, modalContentType } = useModal();
|
||||||
const [selectedPublicListId, setSelectedPublicListId] =
|
const [selectedPublicListId, setSelectedPublicListId] =
|
||||||
@@ -55,7 +65,13 @@ export default function BoardPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data, isSuccess, isLoading } = api.board.byId.useQuery(
|
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,
|
enabled: !!boardId,
|
||||||
},
|
},
|
||||||
@@ -150,7 +166,8 @@ export default function BoardPage() {
|
|||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center">
|
<div className="flex items-center space-x-2">
|
||||||
|
<Filters />
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="mr-2 inline-flex items-center gap-x-1.5 rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-dark-1000 dark:text-dark-50"
|
className="mr-2 inline-flex items-center gap-x-1.5 rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-dark-1000 dark:text-dark-50"
|
||||||
@@ -166,7 +183,7 @@ export default function BoardPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="scrollbar-w-none z-10 flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain pb-5 scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300">
|
<div className="scrollbar-w-none z-0 flex-1 overflow-y-hidden overflow-x-scroll overscroll-contain pb-5 scrollbar scrollbar-track-light-200 scrollbar-thumb-light-400 scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-h-[8px] dark:scrollbar-track-dark-100 dark:scrollbar-thumb-dark-300">
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="ml-[2rem] flex">
|
<div className="ml-[2rem] flex">
|
||||||
<div className="0 mr-5 h-[500px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
<div className="0 mr-5 h-[500px] w-[18rem] animate-pulse rounded-md bg-light-200 dark:bg-dark-200" />
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import { useForm } from "react-hook-form";
|
|
||||||
|
|
||||||
import Modal from "~/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import Button from "~/components/Button";
|
import Button from "~/components/Button";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user