feat: switch next auth and drizzle to supabase (#1)

* feat: add supabase

* feat: setup auth

* feat: test refactoring board queries

* feat: convert to pages router

* feat: convert board router to supabase

* feat: swap out drizzle for supabase in label, list and workspace routers

* feat: swap out drizzle for supabase on the import router

* feat: swap drizzle for supabase on create new card

* feat: switch card router to use supabase

* feat: finish swapping drizzle for supabase

* chore: lint all router files

* fix: signout

* chore: fix types
This commit is contained in:
Henry
2024-04-23 11:57:34 +01:00
committed by GitHub
parent 1711df03e7
commit d15870118d
96 changed files with 3636 additions and 1521 deletions

View File

@@ -0,0 +1,44 @@
import { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { HiEllipsisHorizontal } from "react-icons/hi2";
import { useModal } from "~/providers/modal";
export default function BoardDropdown() {
const { openModal } = useModal();
return (
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="flex h-8 w-8 items-center justify-center rounded-[5px] hover:bg-light-200 dark:hover:bg-dark-200">
<HiEllipsisHorizontal
size={25}
className="text-light-900 dark:text-dark-900"
/>
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="absolute right-0 z-30 mt-2 w-56 origin-top-right rounded-md border border-light-200 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<div className="flex">
<Menu.Item>
<button
onClick={() => openModal("DELETE_BOARD")}
className="m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
>
Delete board
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
);
}

View File

@@ -0,0 +1,49 @@
import { useRouter } from "next/navigation";
import { api } from "~/utils/api";
import { useBoard } from "~/providers/board";
import { useModal } from "~/providers/modal";
export function DeleteBoardConfirmation() {
const router = useRouter();
const { boardData } = useBoard();
const { closeModal } = useModal();
const deleteBoard = api.board.delete.useMutation({
onSuccess: () => {
closeModal();
router.push(`/boards`);
},
});
return (
<>
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
Are you sure you want to delete this board?
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{"This action can't be undone."}
</p>
</div>
<div className="mt-5 flex justify-end sm:mt-6">
<button
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
onClick={() => closeModal()}
>
Cancel
</button>
<button
onClick={() =>
deleteBoard.mutate({
boardPublicId: boardData.publicId,
})
}
className="inline-flex justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
>
Delete
</button>
</div>
</>
);
}

View File

@@ -0,0 +1,56 @@
import { api } from "~/utils/api";
import { useBoard } from "~/providers/board";
import { useModal } from "~/providers/modal";
interface DeleteListConfirmationProps {
listPublicId: string;
}
export function DeleteListConfirmation({
listPublicId,
}: DeleteListConfirmationProps) {
const utils = api.useUtils();
const { boardData } = useBoard();
const { closeModal } = useModal();
const refetchBoard = () =>
utils.board.byId.refetch({ id: boardData.publicId });
const deleteList = api.list.delete.useMutation({
onSuccess: async () => {
closeModal();
await refetchBoard();
},
});
return (
<>
<div className="flex w-full flex-col justify-between pb-4">
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
Are you sure you want to delete this list?
</h2>
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
{"This action can't be undone."}
</p>
</div>
<div className="mt-5 flex justify-end sm:mt-6">
<button
className="mr-4 inline-flex justify-center rounded-md border-[1px] border-light-600 bg-light-50 px-3 py-2 text-sm font-semibold text-neutral-900 shadow-sm focus-visible:outline-none dark:border-dark-600 dark:bg-dark-300 dark:text-dark-1000"
onClick={() => closeModal()}
>
Cancel
</button>
<button
onClick={() =>
deleteList.mutate({
listPublicId,
})
}
className="inline-flex justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
>
Delete
</button>
</div>
</>
);
}

View File

@@ -0,0 +1,106 @@
import { type ReactNode } from "react";
import { HiOutlinePlusSmall } from "react-icons/hi2";
import { Draggable } from "react-beautiful-dnd";
import { useFormik } from "formik";
import { api } from "~/utils/api";
import { useModal } from "~/providers/modal";
import ListDropdown from "./ListDropdown";
interface ListProps {
children: ReactNode;
index: number;
list: List;
setSelectedPublicListId: (publicListId: PublicListId) => void;
}
interface List {
publicId: string;
name: string;
}
interface FormValues {
listPublicId: string;
name: string;
}
type PublicListId = string;
export default function List({
children,
index,
list,
setSelectedPublicListId,
}: ListProps) {
const { openModal } = useModal();
const openNewCardForm = (publicListId: PublicListId) => {
openModal("NEW_CARD");
setSelectedPublicListId(publicListId);
};
const updateList = api.list.update.useMutation();
const formik = useFormik({
initialValues: {
listPublicId: list.publicId,
name: list.name,
},
onSubmit: (values: FormValues) => {
updateList.mutate({
listPublicId: values.listPublicId,
name: values.name,
});
},
enableReinitialize: true,
});
return (
<Draggable key={list.publicId} draggableId={list.publicId} index={index}>
{(provided) => (
<div
key={list.publicId}
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-400 dark:bg-dark-200"
>
<div className="flex justify-between">
<form
onSubmit={formik.handleSubmit}
className="focus-visible:outline-none"
>
<input
type="name"
id="name"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.submitForm}
className="font-mediumfocus:ring-0 mb-4 block border-0 bg-transparent px-4 pt-1 text-sm text-neutral-900 focus-visible:outline-none dark:text-dark-1000"
/>
</form>
<div>
<button
className="mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-400 dark:hover:bg-dark-400"
onClick={() => openNewCardForm(list.publicId)}
>
<HiOutlinePlusSmall
className="h-5 w-5 text-dark-900"
aria-hidden="true"
/>
</button>
<ListDropdown
setSelectedPublicListId={() =>
setSelectedPublicListId(list.publicId)
}
/>
</div>
</div>
{children}
</div>
)}
</Draggable>
);
}

View File

@@ -0,0 +1,52 @@
import { Fragment } from "react";
import { Menu, Transition } from "@headlessui/react";
import { HiEllipsisHorizontal } from "react-icons/hi2";
import { useModal } from "~/providers/modal";
interface ListDropdownProps {
setSelectedPublicListId: () => void;
}
export default function ListDropdown({
setSelectedPublicListId,
}: ListDropdownProps) {
const { openModal } = useModal();
const handleOpenDeleteListConfirmation = () => {
setSelectedPublicListId();
openModal("DELETE_LIST");
};
return (
<Menu as="div" className="relative inline-block text-left">
<div>
<Menu.Button className="mr-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 hover:bg-light-400 dark:hover:bg-dark-400">
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
</Menu.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-100"
enterFrom="transform opacity-0 scale-95"
enterTo="transform opacity-100 scale-100"
leave="transition ease-in duration-75"
leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95"
>
<Menu.Items className="dark-text-dark-1000 absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md border border-light-400 bg-light-50 text-neutral-900 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
<div className="flex">
<Menu.Item>
<button
onClick={handleOpenDeleteListConfirmation}
className="m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm hover:bg-light-400 dark:hover:bg-dark-400"
>
Delete list
</button>
</Menu.Item>
</div>
</Menu.Items>
</Transition>
</Menu>
);
}

View File

@@ -0,0 +1,303 @@
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { api } from "~/utils/api";
import { HiXMark } from "react-icons/hi2";
import { Switch } from "@headlessui/react";
import CheckboxDropdown from "~/components/CheckboxDropdown";
import { useBoard } from "~/providers/board";
import { useModal } from "~/providers/modal";
interface FormData {
title: string;
listPublicId: string;
labelPublicIds: string[];
memberPublicIds: string[];
isCreateAnotherEnabled: boolean;
}
interface NewCardFormProps {
listPublicId: string;
}
function classNames(...classes: string[]): string {
return classes.filter(Boolean).join(" ");
}
export function NewCardForm({ listPublicId }: NewCardFormProps) {
const utils = api.useUtils();
const { boardData } = useBoard();
const { closeModal } = useModal();
const { register, handleSubmit, reset, setValue, watch } = useForm<FormData>({
defaultValues: {
title: "",
listPublicId,
labelPublicIds: [],
memberPublicIds: [],
isCreateAnotherEnabled: false,
},
});
const labelPublicIds = watch("labelPublicIds") || [];
const memberPublicIds = watch("memberPublicIds") || [];
const isCreateAnotherEnabled = watch("isCreateAnotherEnabled");
const refetchBoard = () =>
utils.board.byId.refetch({ id: boardData.publicId });
const createCard = api.card.create.useMutation({
onSuccess: async () => {
try {
await refetchBoard();
if (!isCreateAnotherEnabled) closeModal();
reset({
title: "",
listPublicId: watch("listPublicId"),
labelPublicIds: [],
memberPublicIds: [],
isCreateAnotherEnabled: watch("isCreateAnotherEnabled"),
});
} catch (e) {
console.log(e);
}
},
});
useEffect(() => {
const titleElement: HTMLElement | null =
document?.querySelector<HTMLElement>("#title");
if (titleElement) titleElement.focus();
}, []);
const formattedLabels =
boardData?.labels.map((label) => ({
key: label.publicId,
value: label.name,
selected: labelPublicIds.includes(label.publicId),
})) ?? [];
const formattedLists =
boardData?.lists.map((list) => ({
key: list.publicId,
value: list.name,
selected: list.publicId === watch("listPublicId"),
})) ?? [];
const formattedMembers =
boardData?.workspace?.members?.map((member) => ({
key: member.publicId,
value: member.user?.name ?? "",
selected: memberPublicIds.includes(member.publicId),
})) ?? [];
const onSubmit = (data: FormData) => {
createCard.mutate({
title: data.title,
listPublicId: data.listPublicId,
labelsPublicIds: data.labelPublicIds,
memberPublicIds: data.memberPublicIds,
});
};
const handleToggleCreateAnother = (): void => {
setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled);
};
const handleSelectList = (listPublicId: string): void => {
setValue("listPublicId", listPublicId);
};
const handleSelectMembers = (memberPublicId: string): void => {
const currentIndex = memberPublicIds.indexOf(memberPublicId);
if (currentIndex === -1) {
setValue("memberPublicIds", [...memberPublicIds, memberPublicId]);
} else {
const newMemberPublicIds = [...memberPublicIds];
newMemberPublicIds.splice(currentIndex, 1);
setValue("memberPublicIds", newMemberPublicIds);
}
};
const handleSelectLabels = (labelPublicId: string): void => {
const currentIndex = labelPublicIds.indexOf(labelPublicId);
if (currentIndex === -1) {
setValue("labelPublicIds", [...labelPublicIds, labelPublicId]);
} else {
const newLabelPublicIds = [...labelPublicIds];
newLabelPublicIds.splice(currentIndex, 1);
setValue("labelPublicIds", newLabelPublicIds);
}
};
const selectedList = formattedLists.find((item) => item.selected);
return (
<>
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New card
</h2>
<button
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={() => closeModal()}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div>
<label
htmlFor="title"
className="block pb-2 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000"
>
Title
</label>
<input
id="title"
type="text"
{...register("title")}
className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-600 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
/>
</div>
<div className="mt-2 flex space-x-1">
<div className="w-fit">
<CheckboxDropdown
items={formattedLists}
handleSelect={(list: { key: string }) =>
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">
{selectedList?.value}
</div>
</CheckboxDropdown>
</div>
<div className="w-fit">
<CheckboxDropdown
items={formattedMembers}
handleSelect={(list: { key: string }) =>
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">
{!memberPublicIds.length ? (
"Members"
) : (
<div className="flex -space-x-1 overflow-hidden">
{memberPublicIds.map((memberPublicId) => {
const member = formattedMembers.find(
(member) => member.key === memberPublicId,
);
return (
<span
key={member?.key}
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">
{member?.value
?.split(" ")
.map((namePart) =>
namePart.charAt(0).toUpperCase(),
)
.join("")}
</span>
</span>
);
})}
</div>
)}
</div>
</CheckboxDropdown>
</div>
<div className="w-fit">
<CheckboxDropdown
items={formattedLabels}
handleSelect={(list: { key: string }) =>
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">
{!labelPublicIds.length ? (
"Labels"
) : (
<>
<div
className={
labelPublicIds.length > 1
? "flex -space-x-[2px] overflow-hidden"
: "flex items-center"
}
>
{labelPublicIds.map((labelPublicId) => {
const label = boardData?.labels.find(
(label) => label.publicId === labelPublicId,
);
return (
<>
<svg
fill={label?.colourCode ?? "#3730a3"}
className="h-2 w-2"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx={3} cy={3} r={3} />
</svg>
{labelPublicIds.length === 1 && (
<div className="ml-1">{label?.name}</div>
)}
</>
);
})}
</div>
{labelPublicIds.length > 1 && (
<div className="ml-1">{`${labelPublicIds.length} labels`}</div>
)}
</>
)}
</div>
</CheckboxDropdown>
</div>
</div>
<div className="mt-3 flex items-center justify-end">
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
Create more
</span>
<Switch
checked={isCreateAnotherEnabled}
onChange={handleToggleCreateAnother}
className={classNames(
isCreateAnotherEnabled
? "bg-indigo-600"
: "bg-light-800 dark:bg-dark-800",
"relative inline-flex h-4 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none",
)}
>
<span className="sr-only">Create another</span>
<span
aria-hidden="true"
className={classNames(
isCreateAnotherEnabled ? "translate-x-2" : "translate-x-0",
"pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
)}
/>
</Switch>
</div>
<div className="mt-5 sm:mt-6">
<button
type="submit"
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
>
Create card
</button>
</div>
</form>
</>
);
}

View File

@@ -0,0 +1,124 @@
import { useState, useEffect } from "react";
import { api } from "~/utils/api";
import { HiXMark } from "react-icons/hi2";
import { Switch } from "@headlessui/react";
import { useBoard } from "~/providers/board";
import { useModal } from "~/providers/modal";
import { Formik, Form, Field } from "formik";
interface FormValues {
name: string;
}
interface boardPublicId {
boardPublicId: string;
}
function classNames(...classes: string[]): string {
return classes.filter(Boolean).join(" ");
}
export function NewListForm({ boardPublicId }: boardPublicId) {
const utils = api.useUtils();
const { boardData } = useBoard();
const { closeModal } = useModal();
const [isCreateAnotherEnabled, setIsCreateAnotherEnabled] = useState(false);
const refetchBoard = () =>
utils.board.byId.refetch({ id: boardData.publicId });
const createList = api.list.create.useMutation({
onSuccess: async () => {
try {
await refetchBoard();
if (!isCreateAnotherEnabled) closeModal();
} catch (e) {
console.log(e);
}
},
});
useEffect(() => {
const nameElement: HTMLElement | null =
document?.querySelector<HTMLElement>("#list-name");
if (nameElement) nameElement.focus();
}, []);
return (
<>
<div className="flex w-full items-center justify-between pb-4">
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
New list
</h2>
<button
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
onClick={() => closeModal()}
>
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
</button>
</div>
<Formik
initialValues={{
name: "",
}}
onSubmit={(values: FormValues, { resetForm }) => {
createList.mutate({
name: values.name,
boardPublicId,
});
resetForm();
}}
>
<Form>
<label
htmlFor="list-name"
className="block pb-2 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000"
>
Name
</label>
<Field
id="list-name"
name="name"
className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-600 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
/>
<div className="mt-3 flex items-center justify-end">
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
Create more
</span>
<Switch
checked={isCreateAnotherEnabled}
onChange={setIsCreateAnotherEnabled}
className={classNames(
isCreateAnotherEnabled
? "bg-indigo-600"
: "bg-light-800 dark:bg-dark-800",
"relative inline-flex h-4 w-6 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none",
)}
>
<span className="sr-only">Create more</span>
<span
aria-hidden="true"
className={classNames(
isCreateAnotherEnabled ? "translate-x-2" : "translate-x-0",
"pointer-events-none inline-block h-3 w-3 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out",
)}
/>
</Switch>
</div>
<div className="mt-5 sm:mt-6">
<button
type="submit"
className="inline-flex w-full justify-center rounded-md bg-light-1000 px-3 py-2 text-sm font-semibold text-light-50 shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
>
Create list
</button>
</div>
</Form>
</Formik>
</>
);
}

344
src/views/board/index.tsx Normal file
View File

@@ -0,0 +1,344 @@
import { useState } from "react";
import Link from "next/link";
import { useParams } from "next/navigation";
import { HiOutlinePlusSmall } from "react-icons/hi2";
import {
DragDropContext,
Droppable,
type DropResult,
Draggable,
} from "react-beautiful-dnd";
import { useFormik } from "formik";
import { api } from "~/utils/api";
import { useBoard } from "~/providers/board";
import { useModal } from "~/providers/modal";
import Modal from "~/components/modal";
import BoardDropdown from "./components/BoardDropdown";
import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation";
import { DeleteListConfirmation } from "./components/DeleteListConfirmation";
import List from "./components/List";
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
import { NewCardForm } from "./components/NewCardForm";
import { NewListForm } from "./components/NewListForm";
interface List {
publicId: string;
name: string;
cards?: Card[];
}
interface Card {
publicId: string;
title: string;
labels?: Label[];
members?: Member[];
}
interface Label {
publicId: string;
name: string;
colourCode: string;
}
interface Member {
publicId: string;
user: User;
}
interface User {
name: string;
}
interface FormValues {
boardId: string;
name: string;
}
type PublicListId = string;
export default function BoardPage() {
const params = useParams();
const { boardData, setBoardData, updateCard, updateList } = useBoard();
const { openModal, modalContentType } = useModal();
const [selectedPublicListId, setSelectedPublicListId] =
useState<PublicListId>("");
console.log({ boardData });
const boardId = params?.boardId?.length ? params.boardId[0] : null;
const updateBoard = api.board.update.useMutation();
const formik = useFormik({
initialValues: {
boardId: boardId ?? "",
name: boardData?.name ? boardData.name : "",
},
onSubmit: (values: FormValues) => {
updateBoard.mutate({
boardId: values.boardId,
name: values.name,
});
},
enableReinitialize: true,
});
const { data, isSuccess } = api.board.byId.useQuery(
{ id: boardId ?? "" },
{
enabled: !!boardId,
},
);
if (isSuccess && data) {
console.log({ data });
setBoardData(data);
}
if (!boardId) return <></>;
const openNewListForm = (publicBoardId: string) => {
openModal("NEW_LIST");
setSelectedPublicListId(publicBoardId);
};
const onDragEnd = ({
source,
destination,
draggableId,
type,
}: DropResult): void => {
if (!destination) {
return;
}
if (type === "LIST") {
const updatedLists = Array.from(boardData.lists);
const removedList = updatedLists.splice(source.index, 1)[0];
if (removedList) {
updatedLists.splice(destination.index, 0, removedList);
setBoardData({ ...boardData, lists: updatedLists });
}
updateList({
boardId,
listId: draggableId,
currentIndex: source.index,
newIndex: destination.index,
});
}
if (type === "CARD") {
const updatedLists = Array.from(boardData.lists);
const sourceList = updatedLists.find(
(list) => list.publicId === source.droppableId,
);
const destinationList = updatedLists.find(
(list) => list.publicId === destination.droppableId,
);
const removedCard = sourceList?.cards.splice(source.index, 1)[0];
if (sourceList && destinationList && removedCard) {
destinationList.cards.splice(destination.index, 0, removedCard);
setBoardData({ ...boardData, lists: updatedLists });
}
updateCard({
cardId: draggableId,
newListId: destination.droppableId,
newIndex: destination.index,
});
}
};
return (
<div className="relative flex h-full flex-col">
<div>
<svg
style={{
position: "absolute",
width: "100%",
height: "100%",
top: "0px",
left: "0px",
color: "white",
}}
>
<pattern
id="pattern"
x="0.034759358288862785"
y="3.335370511841166"
width="14.423223834988539"
height="14.423223834988539"
patternUnits="userSpaceOnUse"
patternTransform="translate(-0.45072574484339184,-0.45072574484339184)"
>
<circle
cx="0.45072574484339184"
cy="0.45072574484339184"
r="0.45072574484339184"
fill="#3e3e3e"
></circle>
</pattern>
<rect
x="0"
y="0"
width="100%"
height="100%"
fill="url(#pattern)"
></rect>
</svg>
</div>
<div className="z-20 flex w-full justify-between p-8 ">
<form
onSubmit={formik.handleSubmit}
className="focus-visible:outline-none"
>
<input
type="name"
id="name"
name="name"
value={formik.values.name}
onChange={formik.handleChange}
onBlur={formik.submitForm}
className="block border-0 bg-transparent p-0 py-0 font-medium leading-[2.3rem] tracking-tight text-neutral-900 focus:ring-0 focus-visible:outline-none dark:text-dark-1000 sm:text-[1.2rem]"
/>
</form>
<div className="flex items-center">
<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"
onClick={() => openNewListForm(boardId)}
>
<HiOutlinePlusSmall
className="-mr-0.5 h-5 w-5"
aria-hidden="true"
/>
New list
</button>
<BoardDropdown />
</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">
<DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="all-lists" direction="horizontal" type="LIST">
{(provided) => (
<div
className="flex"
ref={provided.innerRef}
{...provided.droppableProps}
>
<div className="min-w-[2rem]" />
{boardData?.lists?.map((list: List, index) => (
<List
index={index}
key={index}
list={list}
setSelectedPublicListId={(publicListId) =>
setSelectedPublicListId(publicListId)
}
>
<Droppable droppableId={`${list.publicId}`} type="CARD">
{(provided) => (
<div
ref={provided.innerRef}
{...provided.droppableProps}
className="h-full max-h-[calc(100vh-250px)] min-h-[2rem] overflow-y-auto pr-1 scrollbar scrollbar-track-dark-100 scrollbar-thumb-dark-600 scrollbar-track-rounded-[4px] scrollbar-thumb-rounded-[4px] scrollbar-w-[8px]"
>
{list.cards?.map((card, index) => (
<Draggable
key={card.publicId}
draggableId={card.publicId}
index={index}
>
{(provided) => (
<Link
key={card.publicId}
href={`/cards/${card.publicId}`}
className="mb-2 flex !cursor-pointer flex-col rounded-md border border-light-200 bg-light-50 px-3 py-2 text-sm text-neutral-900 dark:border-dark-200 dark:bg-dark-500 dark:text-dark-1000"
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<div>{card.title}</div>
{(card.labels?.length ?? 0) ||
(card.members?.length ?? 0) ? (
<div className="mt-2 flex justify-end space-x-1">
{card.labels?.map((label) => (
<span
key={label.publicId}
className="inline-flex w-fit items-center gap-x-1.5 rounded-full px-2 py-1 text-[10px] font-medium text-neutral-600 ring-1 ring-inset ring-light-600 dark:text-dark-1000 dark:ring-dark-800"
>
<svg
fill={label.colourCode}
className="h-2 w-2"
viewBox="0 0 6 6"
aria-hidden="true"
>
<circle cx={3} cy={3} r={3} />
</svg>
<div>{label.name}</div>
</span>
))}
<div className="isolate flex -space-x-1 overflow-hidden">
{card.members?.map((member) => (
<span
key={member.publicId}
className="inline-flex h-6 w-6 items-center justify-center rounded-full bg-light-900 ring-2 ring-light-50 dark:bg-gray-500 dark:ring-dark-500"
>
<span className="text-[10px] font-medium leading-none text-white">
{member.user.name
.split(" ")
.map((namePart) =>
namePart
.charAt(0)
.toUpperCase(),
)
.join("")}
</span>
</span>
))}
</div>
</div>
) : null}
</Link>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</List>
))}
<div className="min-w-[0.75rem]" />
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
</div>
<Modal>
{modalContentType === "DELETE_BOARD" && <DeleteBoardConfirmation />}
{modalContentType === "DELETE_LIST" && (
<DeleteListConfirmation listPublicId={selectedPublicListId} />
)}
{modalContentType === "NEW_CARD" && (
<NewCardForm listPublicId={selectedPublicListId} />
)}
{modalContentType === "NEW_LIST" && (
<NewListForm boardPublicId={boardId} />
)}
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
</Modal>
</div>
);
}