feat: board slugs
This commit is contained in:
@@ -1,44 +1,27 @@
|
||||
import { Fragment } from "react";
|
||||
import { Menu, Transition } from "@headlessui/react";
|
||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||
import { HiEllipsisHorizontal, HiLink, HiOutlineTrash } from "react-icons/hi2";
|
||||
|
||||
import Dropdown from "~/components/Dropdown";
|
||||
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>
|
||||
<Dropdown
|
||||
items={[
|
||||
{
|
||||
label: "Edit board URL",
|
||||
action: () => openModal("UPDATE_BOARD_SLUG"),
|
||||
icon: <HiLink className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
{
|
||||
label: "Delete board",
|
||||
action: () => openModal("DELETE_BOARD"),
|
||||
icon: <HiOutlineTrash className="h-[16px] w-[16px] text-dark-900" />,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
122
apps/web/src/views/board/components/UpdateBoardSlugForm.tsx
Normal file
122
apps/web/src/views/board/components/UpdateBoardSlugForm.tsx
Normal file
@@ -0,0 +1,122 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { HiXMark } from "react-icons/hi2";
|
||||
import { z } from "zod";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
import { useBoard } from "~/providers/board";
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { usePopup } from "~/providers/popup";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
const schema = z.object({
|
||||
slug: z
|
||||
.string()
|
||||
.min(3, {
|
||||
message: "Board URL must be at least 3 characters long",
|
||||
})
|
||||
.max(60, { message: "Board URL cannot exceed 60 characters" })
|
||||
.regex(/^(?![-]+$)[a-zA-Z0-9-]+$/, {
|
||||
message: "Board URL can only contain letters, numbers, and hyphens",
|
||||
}),
|
||||
});
|
||||
|
||||
type FormValues = z.infer<typeof schema>;
|
||||
|
||||
export function UpdateBoardSlugForm({
|
||||
boardPublicId,
|
||||
workspaceSlug,
|
||||
boardSlug,
|
||||
}: {
|
||||
boardPublicId: string;
|
||||
workspaceSlug: string;
|
||||
boardSlug: string;
|
||||
}) {
|
||||
const { closeModal } = useModal();
|
||||
const { showPopup } = usePopup();
|
||||
const { refetchBoard } = useBoard();
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { isDirty, errors },
|
||||
} = useForm<FormValues>({
|
||||
resolver: zodResolver(schema),
|
||||
values: {
|
||||
slug: boardSlug,
|
||||
},
|
||||
mode: "onChange",
|
||||
});
|
||||
|
||||
const updateBoardSlug = api.board.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
await refetchBoard();
|
||||
closeModal();
|
||||
},
|
||||
onError: () => {
|
||||
closeModal();
|
||||
showPopup({
|
||||
header: "Unable to update board URL",
|
||||
message: "Please try again later, or contact customer support.",
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const nameElement: HTMLElement | null =
|
||||
document.querySelector<HTMLElement>("#board-slug");
|
||||
if (nameElement) nameElement.focus();
|
||||
}, []);
|
||||
|
||||
const onSubmit = (data: FormValues) => {
|
||||
updateBoardSlug.mutate({
|
||||
slug: data.slug,
|
||||
boardPublicId,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="px-5 pt-5">
|
||||
<div className="flex w-full items-center justify-between pb-4">
|
||||
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||
Edit board URL
|
||||
</h2>
|
||||
<button
|
||||
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<Input
|
||||
id="board-slug"
|
||||
{...register("slug")}
|
||||
errorMessage={errors.slug?.message}
|
||||
prefix={`kan.bn/${workspaceSlug}/`}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-12 flex items-center justify-end border-t border-light-600 px-5 pb-5 pt-5 dark:border-dark-600">
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateBoardSlug.isPending}
|
||||
disabled={
|
||||
!isDirty ||
|
||||
updateBoardSlug.isPending ||
|
||||
errors.slug?.message !== undefined
|
||||
}
|
||||
>
|
||||
Update
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
@@ -26,6 +26,7 @@ import Filters from "./components/Filters";
|
||||
import List from "./components/List";
|
||||
import { NewCardForm } from "./components/NewCardForm";
|
||||
import { NewListForm } from "./components/NewListForm";
|
||||
import { UpdateBoardSlugForm } from "./components/UpdateBoardSlugForm";
|
||||
|
||||
type PublicListId = string;
|
||||
|
||||
@@ -313,6 +314,13 @@ export default function BoardPage() {
|
||||
<NewListForm boardPublicId={boardId} />
|
||||
)}
|
||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||
{modalContentType === "UPDATE_BOARD_SLUG" && (
|
||||
<UpdateBoardSlugForm
|
||||
boardPublicId={boardId}
|
||||
workspaceSlug={workspace.slug}
|
||||
boardSlug={boardData.slug}
|
||||
/>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function WorkspaceSlugPage() {
|
||||
workspaceSlug,
|
||||
}: {
|
||||
isLoading: boolean;
|
||||
boards: { publicId: string; name: string }[];
|
||||
boards: { publicId: string; name: string; slug: string }[];
|
||||
workspaceSlug: string;
|
||||
}) => {
|
||||
if (isLoading)
|
||||
@@ -45,7 +45,7 @@ export default function WorkspaceSlugPage() {
|
||||
{boards.map((board) => (
|
||||
<Link
|
||||
key={board.publicId}
|
||||
href={`/${workspaceSlug}/${board.publicId}`}
|
||||
href={`/${workspaceSlug}/${board.slug}`}
|
||||
className="h-full"
|
||||
>
|
||||
<div className="relative flex h-full w-full items-center justify-center rounded-md border border-dashed border-light-400 bg-light-50 shadow-sm hover:bg-light-200 dark:border-dark-600 dark:bg-dark-50 dark:hover:bg-dark-100">
|
||||
|
||||
Reference in New Issue
Block a user