feat: monorepo
This commit is contained in:
40
apps/web/src/views/boards/components/BoardsList.tsx
Normal file
40
apps/web/src/views/boards/components/BoardsList.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import Link from "next/link";
|
||||
|
||||
import { api } from "~/utils/api";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import PatternedBackground from "~/components/PatternedBackground";
|
||||
|
||||
export function BoardsList() {
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const { data, isLoading } = api.board.all.useQuery(
|
||||
{ workspacePublicId: workspace?.publicId },
|
||||
{ enabled: workspace?.publicId ? true : false },
|
||||
);
|
||||
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xxl:grid-cols-5">
|
||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||
<div className="mr-5 flex h-[150px] w-full animate-pulse rounded-md bg-light-200 dark:bg-dark-100" />
|
||||
</div>
|
||||
);
|
||||
|
||||
if (data?.length === 0) return <></>;
|
||||
|
||||
return (
|
||||
<div className="grid w-full grid-cols-1 gap-4 sm:grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 xxl:grid-cols-5">
|
||||
{data?.map((board) => (
|
||||
<Link key={board.publicId} href={`boards/${board.publicId}`}>
|
||||
<div className="align-center relative mr-5 flex h-[150px] 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">
|
||||
<PatternedBackground />
|
||||
<p className="text-md px-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||
{board.name}
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
238
apps/web/src/views/boards/components/ImportBoardsForm.tsx
Normal file
238
apps/web/src/views/boards/components/ImportBoardsForm.tsx
Normal file
@@ -0,0 +1,238 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { api } from "~/utils/api";
|
||||
import { Listbox, Transition } from "@headlessui/react";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
|
||||
import { FaTrello } from "react-icons/fa";
|
||||
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
||||
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
|
||||
import Button from "~/components/Button";
|
||||
import Input from "~/components/Input";
|
||||
|
||||
interface TrelloFormValues {
|
||||
apiKey: string;
|
||||
token: string;
|
||||
}
|
||||
|
||||
const sources = [{ source: "Trello" }];
|
||||
|
||||
const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
||||
const { control, handleSubmit } = useForm({
|
||||
defaultValues: {
|
||||
source: "Trello",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = () => {
|
||||
handleNextStep();
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="px-5">
|
||||
<Controller
|
||||
name="source"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Listbox {...field}>
|
||||
{({ open }) => (
|
||||
<>
|
||||
<div className="relative">
|
||||
<Listbox.Button className="focus-ring-light-700 block w-full rounded-md border-0 bg-dark-300 bg-white/5 px-4 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6">
|
||||
<span className="flex items-center">
|
||||
<FaTrello />
|
||||
<span className="ml-2 block truncate">
|
||||
{field.value}
|
||||
</span>
|
||||
</span>
|
||||
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
|
||||
<HiChevronUpDown
|
||||
className="h-5 w-5 text-gray-400"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</span>
|
||||
</Listbox.Button>
|
||||
|
||||
<Transition
|
||||
show={open}
|
||||
as={Fragment}
|
||||
leave="transition ease-in duration-100"
|
||||
leaveFrom="opacity-100"
|
||||
leaveTo="opacity-0"
|
||||
>
|
||||
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-light-50 py-1 text-base text-neutral-900 shadow-lg ring-1 ring-light-600 ring-opacity-5 focus:outline-none dark:bg-dark-300 dark:text-dark-1000 sm:text-sm">
|
||||
{sources.map(({ source }, index) => (
|
||||
<Listbox.Option
|
||||
key={`source_${index}`}
|
||||
className="relative cursor-default select-none px-1"
|
||||
value={source}
|
||||
>
|
||||
<div className="flex items-center rounded-[5px] p-1 hover:bg-light-200 dark:hover:bg-dark-400">
|
||||
<FaTrello className="ml-1" />
|
||||
<span className="ml-2 block truncate font-normal">
|
||||
{source}
|
||||
</span>
|
||||
</div>
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
</Transition>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Listbox>
|
||||
)}
|
||||
/>
|
||||
</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">Select source</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
const ImportTrello: React.FC = () => {
|
||||
const utils = api.useUtils();
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
const [token, setToken] = useState("");
|
||||
const { closeModal } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const refetchBoards = () => utils.board.all.refetch();
|
||||
|
||||
const boards = api.import.trello.getBoards.useQuery(
|
||||
{ apiKey, token },
|
||||
{
|
||||
enabled: apiKey && token ? true : false,
|
||||
},
|
||||
);
|
||||
|
||||
const handleSetAuthDetails = (apiKey: string, token: string) => {
|
||||
setApiKey(apiKey);
|
||||
setToken(token);
|
||||
};
|
||||
|
||||
const importBoards = api.import.trello.importBoards.useMutation({
|
||||
onSuccess: async () => {
|
||||
try {
|
||||
await refetchBoards();
|
||||
closeModal();
|
||||
} catch (e) {
|
||||
console.log(e);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const { register, handleSubmit } = useForm<TrelloFormValues>({
|
||||
defaultValues: {
|
||||
apiKey: "",
|
||||
token: "",
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (values: TrelloFormValues) => {
|
||||
handleSetAuthDetails(values.apiKey, values.token);
|
||||
};
|
||||
|
||||
const { register: registerBoards, handleSubmit: handleSubmitBoards } =
|
||||
useForm({
|
||||
defaultValues: Object.fromEntries(
|
||||
boards?.data?.map((board) => [board.id, true]) ?? [],
|
||||
),
|
||||
});
|
||||
|
||||
const onSubmitBoards = (values: Record<string, boolean>) => {
|
||||
const boardIds = Object.keys(values).filter((key) => values[key] === true);
|
||||
|
||||
importBoards.mutate({
|
||||
boardIds,
|
||||
apiKey,
|
||||
token,
|
||||
workspacePublicId: workspace?.publicId,
|
||||
});
|
||||
};
|
||||
|
||||
if (boards?.data?.length)
|
||||
return (
|
||||
<form onSubmit={handleSubmitBoards(onSubmitBoards)}>
|
||||
<div className="h-[105px] overflow-scroll px-5">
|
||||
{boards.data.map((board) => (
|
||||
<div key={board.id}>
|
||||
<label
|
||||
className="flex cursor-pointer items-center rounded-[5px] p-2 hover:bg-light-100 dark:hover:bg-dark-300"
|
||||
htmlFor={board.id}
|
||||
>
|
||||
<input
|
||||
id={board.id}
|
||||
type="checkbox"
|
||||
className="h-[14px] w-[14px] rounded bg-transparent ring-0 focus:outline-none focus:ring-0 focus:ring-offset-0"
|
||||
{...registerBoards(board.id)}
|
||||
/>
|
||||
<span className="ml-3 text-sm text-neutral-900 dark:text-dark-1000">
|
||||
{board.name}
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</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={importBoards.isPending}>
|
||||
Import boards
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="text-neutral-900 dark:text-dark-1000"
|
||||
>
|
||||
<div className="space-y-4 px-5">
|
||||
<Input id="apiKey" placeholder="API key" {...register("apiKey")} />
|
||||
<Input id="token" placeholder="Token" {...register("token")} />
|
||||
</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={boards.isLoading}>
|
||||
Fetch boards
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export function ImportBoardsForm() {
|
||||
const { closeModal } = useModal();
|
||||
const [step, setStep] = useState(1);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex w-full items-center justify-between px-5 pb-4 pt-5">
|
||||
<h2 className="text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
||||
New import
|
||||
</h2>
|
||||
<button
|
||||
className="rounded p-1 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||
onClick={() => closeModal()}
|
||||
>
|
||||
<HiXMark size={18} className="text-dark-900" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{step === 1 && <SelectSource handleNextStep={() => setStep(step + 1)} />}
|
||||
{step === 2 && <ImportTrello />}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
70
apps/web/src/views/boards/components/NewBoardForm.tsx
Normal file
70
apps/web/src/views/boards/components/NewBoardForm.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useForm } from "react-hook-form";
|
||||
import { HiXMark } from "react-icons/hi2";
|
||||
|
||||
import { type NewBoardInput } from "@kan/api/types";
|
||||
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import { api } from "~/utils/api";
|
||||
|
||||
export function NewBoardForm() {
|
||||
const utils = api.useUtils();
|
||||
const { closeModal } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
const { register, handleSubmit } = useForm<NewBoardInput>({
|
||||
defaultValues: {
|
||||
name: "",
|
||||
workspacePublicId: workspace?.publicId || "",
|
||||
},
|
||||
});
|
||||
|
||||
const refetchBoards = () => utils.board.all.refetch();
|
||||
|
||||
const createBoard = api.board.create.useMutation({
|
||||
onSuccess: async () => {
|
||||
closeModal();
|
||||
await refetchBoards();
|
||||
},
|
||||
});
|
||||
|
||||
const onSubmit = (data: NewBoardInput) => {
|
||||
createBoard.mutate(data);
|
||||
};
|
||||
|
||||
return (
|
||||
<form onSubmit={handleSubmit(onSubmit)}>
|
||||
<div className="px-5 pt-5">
|
||||
<div className="text-neutral-9000 flex w-full items-center justify-between pb-4 dark:text-dark-1000">
|
||||
<h2 className="text-sm font-bold">New board</h2>
|
||||
<button
|
||||
className="hover:bg-li ght-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
closeModal();
|
||||
}}
|
||||
>
|
||||
<HiXMark size={18} className="dark:text-dark-9000 text-light-900" />
|
||||
</button>
|
||||
</div>
|
||||
<input
|
||||
id="name"
|
||||
placeholder="Name"
|
||||
{...register("name", { required: true })}
|
||||
className="block w-full rounded-md border-0 bg-white/5 py-1.5 text-neutral-900 placeholder-dark-800 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-600 dark:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
||||
/>
|
||||
</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"
|
||||
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 board
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
67
apps/web/src/views/boards/index.tsx
Normal file
67
apps/web/src/views/boards/index.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
||||
import { BoardsList } from "./components/BoardsList";
|
||||
|
||||
import { useModal } from "~/providers/modal";
|
||||
import { useWorkspace } from "~/providers/workspace";
|
||||
import Modal from "~/components/modal";
|
||||
|
||||
import { PageHead } from "~/components/PageHead";
|
||||
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
||||
import { NewBoardForm } from "./components/NewBoardForm";
|
||||
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||
|
||||
export default function BoardsPage() {
|
||||
const { openModal, modalContentType } = useModal();
|
||||
const { workspace } = useWorkspace();
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHead title={`Boards | ${workspace?.name ?? "Workspace"}`} />
|
||||
<div className="p-8">
|
||||
<div className="mb-8 flex w-full justify-between">
|
||||
<h1 className="font-medium tracking-tight text-neutral-900 dark:text-dark-1000 sm:text-[1.2rem]">
|
||||
Boards
|
||||
</h1>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="button"
|
||||
className="bg-dark-3000 mr-2 flex items-center gap-x-1.5 rounded-md border-[1px] border-light-600 px-3 py-2 text-sm text-neutral-900 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:border-dark-600 dark:text-dark-1000"
|
||||
onClick={() => openModal("IMPORT_BOARDS")}
|
||||
>
|
||||
<div className="flex h-5 w-5 items-center">
|
||||
<HiArrowDownTray
|
||||
className="-mr-0.5 h-4 w-4"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
Import
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="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={() => openModal("NEW_BOARD")}
|
||||
>
|
||||
<div className="h-5 w-5 items-center">
|
||||
<HiOutlinePlusSmall
|
||||
className="-mr-0.5 h-5 w-5"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
</div>
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Modal>
|
||||
{modalContentType === "NEW_BOARD" && <NewBoardForm />}
|
||||
{modalContentType === "IMPORT_BOARDS" && <ImportBoardsForm />}
|
||||
{modalContentType === "NEW_WORKSPACE" && <NewWorkspaceForm />}
|
||||
</Modal>
|
||||
|
||||
<div className="flex flex-row">
|
||||
<BoardsList />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user