Compare commits
15 Commits
fix/remove
...
feat/supab
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dfbe81f49c | ||
|
|
850b0fa876 | ||
|
|
0c1de23a19 | ||
|
|
4e344a5ae1 | ||
|
|
227e76524b | ||
|
|
4e1e99cbac | ||
|
|
bfb5e49981 | ||
|
|
69af62e032 | ||
|
|
474c327def | ||
|
|
b695f7bdf3 | ||
|
|
1712a1a41b | ||
|
|
e048e243d4 | ||
|
|
6178b4f309 | ||
|
|
1ba6fffd8b | ||
|
|
b54cfe8f91 |
5
.vscode/settings.json
vendored
5
.vscode/settings.json
vendored
@@ -1,3 +1,6 @@
|
|||||||
{
|
{
|
||||||
"spellright.addToSystemDictionary": true
|
"spellright.addToSystemDictionary": true,
|
||||||
|
"editor.codeActionsOnSave": {
|
||||||
|
"source.fixAll": "explicit"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
50
package.json
50
package.json
@@ -16,22 +16,22 @@
|
|||||||
"src/email"
|
"src/email"
|
||||||
],
|
],
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/drizzle-adapter": "^0.3.12",
|
"@edge-runtime/cookies": "^4.1.1",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@planetscale/database": "^1.11.0",
|
|
||||||
"@react-email/render": "^0.0.10",
|
"@react-email/render": "^0.0.10",
|
||||||
"@t3-oss/env-nextjs": "^0.7.0",
|
"@supabase/ssr": "^0.3.0",
|
||||||
|
"@supabase/supabase-js": "^2.42.0",
|
||||||
|
"@t3-oss/env-nextjs": "^0.9.2",
|
||||||
"@tailwindcss/forms": "^0.5.6",
|
"@tailwindcss/forms": "^0.5.6",
|
||||||
"@tanstack/react-query": "^4.32.6",
|
"@tanstack/react-query": "^5.25.0",
|
||||||
"@trpc/client": "^10.37.1",
|
"@trpc/client": "next",
|
||||||
"@trpc/next": "^10.37.1",
|
"@trpc/next": "next",
|
||||||
"@trpc/react-query": "^10.37.1",
|
"@trpc/react-query": "next",
|
||||||
"@trpc/server": "^10.37.1",
|
"@trpc/server": "next",
|
||||||
"@vercel/postgres": "^0.7.2",
|
"@vercel/postgres": "^0.7.2",
|
||||||
"drizzle-orm": "^0.28.5",
|
"drizzle-orm": "^0.28.5",
|
||||||
"formik": "^2.4.5",
|
"formik": "^2.4.5",
|
||||||
"next": "^13.5.4",
|
"next": "^14.1.3",
|
||||||
"next-auth": "0.0.0-manual.cfb42ae7",
|
|
||||||
"postgres": "^3.4.4",
|
"postgres": "^3.4.4",
|
||||||
"react": "18.2.0",
|
"react": "18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
@@ -44,26 +44,28 @@
|
|||||||
"zod": "^3.22.4"
|
"zod": "^3.22.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/eslint": "^8.44.2",
|
"@types/cookie": "^0.6.0",
|
||||||
"@types/node": "^18.16.0",
|
"@types/eslint": "^8.56.2",
|
||||||
"@types/react": "^18.2.20",
|
"@types/node": "^20.11.20",
|
||||||
|
"@types/react": "^18.2.57",
|
||||||
"@types/react-beautiful-dnd": "^13.1.7",
|
"@types/react-beautiful-dnd": "^13.1.7",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^6.3.0",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"autoprefixer": "^10.4.14",
|
"autoprefixer": "^10.4.14",
|
||||||
"dotenv-cli": "^7.3.0",
|
"dotenv-cli": "^7.3.0",
|
||||||
"drizzle-kit": "^0.19.13",
|
"drizzle-kit": "^0.19.13",
|
||||||
"eslint": "^8.47.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-next": "^13.5.4",
|
"eslint-config-next": "^14.1.3",
|
||||||
"mysql2": "^3.6.1",
|
"pg": "^8.11.3",
|
||||||
"postcss": "^8.4.27",
|
"postcss": "^8.4.34",
|
||||||
"prettier": "^3.0.0",
|
"prettier": "^3.2.5",
|
||||||
"prettier-plugin-tailwindcss": "^0.5.1",
|
"prettier-plugin-tailwindcss": "^0.5.11",
|
||||||
|
"supabase": "^1.153.4",
|
||||||
"tailwind": "^4.0.0",
|
"tailwind": "^4.0.0",
|
||||||
"tailwind-scrollbar": "^3.0.5",
|
"tailwind-scrollbar": "^3.0.5",
|
||||||
"tailwindcss": "^3.3.3",
|
"tailwindcss": "^3.4.1",
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.4.2"
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.22.0"
|
"initVersion": "7.22.0"
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
export { GET, POST } from "~/server/auth";
|
|
||||||
|
|
||||||
export const runtime = "edge";
|
|
||||||
export const preferredRegion = 'lhr1';
|
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { WorkspaceProvider } from "~/app/providers/workspace";
|
|
||||||
import Dashboard from "~/app/components/dashboard";
|
|
||||||
|
|
||||||
export default function Layout(props: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<WorkspaceProvider>
|
|
||||||
<Dashboard>{props.children}</Dashboard>
|
|
||||||
</WorkspaceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { WorkspaceProvider } from "~/app/providers/workspace";
|
|
||||||
import Dashboard from "~/app/components/dashboard";
|
|
||||||
|
|
||||||
export default function Layout(props: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<WorkspaceProvider>
|
|
||||||
<Dashboard>{props.children}</Dashboard>;
|
|
||||||
</WorkspaceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { redirect } from "next/navigation";
|
|
||||||
|
|
||||||
import { auth } from "~/server/auth";
|
|
||||||
|
|
||||||
import SideNavigation from "./SideNavigation";
|
|
||||||
import FeedbackButton from "./FeedbackButton";
|
|
||||||
|
|
||||||
export default async function Dashboard(props: { children: React.ReactNode }) {
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
if (!session?.user) {
|
|
||||||
redirect("/api/auth/signin");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<main className="bg-light-100 flex h-screen flex-col items-center dark:bg-dark-50">
|
|
||||||
<div className="border-light-600 m-auto flex h-16 w-full justify-between border-b px-5 py-2 align-middle dark:border-dark-600">
|
|
||||||
<div className="my-auto flex w-full items-center justify-between">
|
|
||||||
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
|
||||||
kan.bn
|
|
||||||
</h1>
|
|
||||||
<FeedbackButton />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex h-full w-full">
|
|
||||||
<SideNavigation user={session.user} />
|
|
||||||
<div className="w-full overflow-hidden">{props.children}</div>
|
|
||||||
</div>
|
|
||||||
</main>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import "~/styles/globals.css";
|
|
||||||
|
|
||||||
import { Plus_Jakarta_Sans } from "next/font/google";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { TRPCReactProvider } from "~/trpc/react";
|
|
||||||
import { ModalProvider } from "~/app/providers/modal";
|
|
||||||
import { BoardProvider } from "~/app/providers/board";
|
|
||||||
import { ThemeProvider } from "./providers/theme";
|
|
||||||
|
|
||||||
const jakarta = Plus_Jakarta_Sans({
|
|
||||||
subsets: ["latin"],
|
|
||||||
display: "swap",
|
|
||||||
variable: "--font-plus-jakarta-sans",
|
|
||||||
});
|
|
||||||
|
|
||||||
export const metadata = {
|
|
||||||
title: "Kan",
|
|
||||||
description: "The open source Trello alternative",
|
|
||||||
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function RootLayout({
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<html lang="en" className={`${jakarta.variable}`}>
|
|
||||||
<body className="h-screen overflow-hidden font-sans">
|
|
||||||
<ThemeProvider>
|
|
||||||
<TRPCReactProvider headers={headers()}>
|
|
||||||
<ModalProvider>
|
|
||||||
<BoardProvider>{children}</BoardProvider>
|
|
||||||
</ModalProvider>
|
|
||||||
</TRPCReactProvider>
|
|
||||||
</ThemeProvider>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { WorkspaceProvider } from "~/app/providers/workspace";
|
|
||||||
import Dashboard from "~/app/components/dashboard";
|
|
||||||
|
|
||||||
export default function Layout(props: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<WorkspaceProvider>
|
|
||||||
<Dashboard>{props.children}</Dashboard>;
|
|
||||||
</WorkspaceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default function Home() {
|
|
||||||
return <main></main>;
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
import { WorkspaceProvider } from "~/app/providers/workspace";
|
|
||||||
import Dashboard from "~/app/components/dashboard";
|
|
||||||
|
|
||||||
export default function Layout(props: { children: React.ReactNode }) {
|
|
||||||
return (
|
|
||||||
<WorkspaceProvider>
|
|
||||||
<Dashboard>{props.children}</Dashboard>;
|
|
||||||
</WorkspaceProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
@@ -35,13 +33,13 @@ export default function CheckboxDropdown({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-200 absolute left-0 top-[26px] z-10 mt-2 w-56 origin-top-left rounded-md border-[1px] shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
<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">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{items?.map((item) => (
|
{items?.map((item) => (
|
||||||
<Menu.Item key={item.key}>
|
<Menu.Item key={item.key}>
|
||||||
<div
|
<div
|
||||||
key={item.key}
|
key={item.key}
|
||||||
className="hover:bg-light-200 flex items-center rounded-[5px] p-2 dark:hover:bg-dark-300"
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
handleSelect({ key: item.key });
|
handleSelect({ key: item.key });
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTheme } from "~/app/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
|
|
||||||
import LottieIcon from "~/app/components/LottieIcon";
|
import LottieIcon from "~/components/LottieIcon";
|
||||||
import chatIconLight from "~/app/assets/chat-light.json";
|
import chatIconLight from "~/assets/chat-light.json";
|
||||||
import chatIconDark from "~/app/assets/chat-dark.json";
|
import chatIconDark from "~/assets/chat-dark.json";
|
||||||
|
|
||||||
const FeedbackButton: React.FC = () => {
|
const FeedbackButton: React.FC = () => {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
@@ -21,7 +19,7 @@ const FeedbackButton: React.FC = () => {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
className="bg-light-50 border-light-600 flex items-center rounded-md border-[1px] px-2.5 py-1.5 text-sm font-normal text-neutral-900 shadow-sm dark:border-dark-600 dark:bg-dark-50 dark:text-dark-1000"
|
className="flex items-center rounded-md border-[1px] border-light-600 bg-light-50 px-2.5 py-1.5 text-sm font-normal text-neutral-900 shadow-sm dark:border-dark-600 dark:bg-dark-50 dark:text-dark-1000"
|
||||||
>
|
>
|
||||||
<LottieIcon
|
<LottieIcon
|
||||||
index={index}
|
index={index}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Lottie from "react-lottie-player";
|
import Lottie from "react-lottie-player";
|
||||||
|
|
||||||
type IconProps = {
|
type IconProps = {
|
||||||
@@ -1,11 +1,10 @@
|
|||||||
"use client";
|
import { useEffect } from "react";
|
||||||
|
import { api } from "~/utils/api";
|
||||||
import { api } from "~/trpc/react";
|
|
||||||
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
|
|
||||||
@@ -20,7 +19,7 @@ export function NewWorkspaceForm() {
|
|||||||
const createWorkspace = api.workspace.create.useMutation({
|
const createWorkspace = api.workspace.create.useMutation({
|
||||||
onSuccess: (values) => {
|
onSuccess: (values) => {
|
||||||
try {
|
try {
|
||||||
if (values?.publicId) {
|
if (values?.publicId && values.name) {
|
||||||
switchWorkspace({ publicId: values.publicId, name: values.name });
|
switchWorkspace({ publicId: values.publicId, name: values.name });
|
||||||
closeModal();
|
closeModal();
|
||||||
}
|
}
|
||||||
@@ -30,15 +29,23 @@ export function NewWorkspaceForm() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const nameElement: HTMLElement | null =
|
||||||
|
document?.querySelector<HTMLElement>("#workspace-name");
|
||||||
|
if (nameElement) nameElement.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex w-full justify-between pb-4">
|
<div className="flex w-full items-center justify-between pb-4">
|
||||||
<h2 className="text-sm font-bold text-dark-1000">New workspace</h2>
|
<h2 className="text-sm font-bold text-neutral-900 dark:text-dark-1000">
|
||||||
|
New workspace
|
||||||
|
</h2>
|
||||||
<button
|
<button
|
||||||
className="rounded p-1 hover:bg-dark-300 focus:outline-none"
|
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-dark-900" />
|
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -54,20 +61,20 @@ export function NewWorkspaceForm() {
|
|||||||
>
|
>
|
||||||
<Form>
|
<Form>
|
||||||
<label
|
<label
|
||||||
htmlFor="name"
|
htmlFor="workspace-name"
|
||||||
className="block pb-2 text-sm font-normal leading-6 text-dark-1000"
|
className="block pb-2 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000"
|
||||||
>
|
>
|
||||||
Name
|
Name
|
||||||
</label>
|
</label>
|
||||||
<Field
|
<Field
|
||||||
id="name"
|
id="workspace-name"
|
||||||
name="name"
|
name="name"
|
||||||
className="block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 text-dark-1000 shadow-sm ring-1 ring-inset ring-dark-700 focus:ring-2 focus:ring-inset focus:ring-dark-700 sm:text-sm sm:leading-6"
|
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-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="inline-flex w-full justify-center rounded-md bg-dark-1000 px-3 py-2 text-sm font-semibold text-dark-50 shadow-sm focus-visible:outline-none"
|
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 workspace
|
Create workspace
|
||||||
</button>
|
</button>
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import LottieIcon from "~/app/components/LottieIcon";
|
import LottieIcon from "~/components/LottieIcon";
|
||||||
|
|
||||||
function classNames(...classes: string[]): string {
|
function classNames(...classes: string[]): string {
|
||||||
return classes.filter(Boolean).join(" ");
|
return classes.filter(Boolean).join(" ");
|
||||||
@@ -29,7 +27,7 @@ const Button: React.FC<{
|
|||||||
onMouseEnter={handleMouseEnter}
|
onMouseEnter={handleMouseEnter}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
current ? "bg-light-200 dark:bg-dark-200" : " dark:bg-dark-50",
|
current ? "bg-light-200 dark:bg-dark-200" : " dark:bg-dark-50",
|
||||||
"hover:bg-light-200 group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6 text-neutral-900 dark:text-dark-1000 dark:hover:bg-dark-200",
|
"group flex items-center gap-x-3 rounded-md p-1.5 text-sm font-normal leading-6 text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-200",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
<LottieIcon index={index} json={json} isPlaying={isHovered} />
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { usePathname } from "next/navigation";
|
import { usePathname } from "next/navigation";
|
||||||
|
|
||||||
import { useTheme } from "~/app/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
|
|
||||||
import boardsIconDark from "~/app/assets/boards-dark.json";
|
import boardsIconDark from "~/assets/boards-dark.json";
|
||||||
import boardsIconLight from "~/app/assets/boards-light.json";
|
import boardsIconLight from "~/assets/boards-light.json";
|
||||||
import membersIconDark from "~/app/assets/members-dark.json";
|
import membersIconDark from "~/assets/members-dark.json";
|
||||||
import membersIconLight from "~/app/assets/members-light.json";
|
import membersIconLight from "~/assets/members-light.json";
|
||||||
import settingsIconDark from "~/app/assets/settings-dark.json";
|
import settingsIconDark from "~/assets/settings-dark.json";
|
||||||
import settingsIconLight from "~/app/assets/settings-light.json";
|
import settingsIconLight from "~/assets/settings-light.json";
|
||||||
|
|
||||||
import ReactiveButton from "~/app/components/ReactiveButton";
|
import ReactiveButton from "~/components/ReactiveButton";
|
||||||
import UserMenu from "~/app/components/UserMenu";
|
import UserMenu from "~/components/UserMenu";
|
||||||
import WorkspaceMenu from "~/app/components/WorkspaceMenu";
|
import WorkspaceMenu from "~/components/WorkspaceMenu";
|
||||||
|
|
||||||
interface SideNavigationProps {
|
interface SideNavigationProps {
|
||||||
user: UserType;
|
user: UserType;
|
||||||
@@ -48,7 +46,7 @@ export default function SideNavigation({ user }: SideNavigationProps) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<nav className="border-light-600 flex w-72 flex-col justify-between border-r px-3 pb-3 pt-5 dark:border-dark-600">
|
<nav className="flex w-72 flex-col justify-between border-r border-light-600 px-3 pb-3 pt-5 dark:border-dark-600">
|
||||||
<div>
|
<div>
|
||||||
<WorkspaceMenu />
|
<WorkspaceMenu />
|
||||||
<ul role="list" className="space-y-1">
|
<ul role="list" className="space-y-1">
|
||||||
@@ -56,7 +54,7 @@ export default function SideNavigation({ user }: SideNavigationProps) {
|
|||||||
<li key={item.name}>
|
<li key={item.name}>
|
||||||
<ReactiveButton
|
<ReactiveButton
|
||||||
href={item.href}
|
href={item.href}
|
||||||
current={pathname.includes(item.href)}
|
current={pathname?.includes(item.href)}
|
||||||
name={item.name}
|
name={item.name}
|
||||||
json={item.icon}
|
json={item.icon}
|
||||||
/>
|
/>
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { signOut } from "next-auth/react";
|
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { useTheme } from "~/app/providers/theme";
|
import { useTheme } from "~/providers/theme";
|
||||||
|
import createClient from "~/utils/supabase/client";
|
||||||
interface UserMenuProps {
|
interface UserMenuProps {
|
||||||
imageUrl: string | undefined;
|
imageUrl: string | undefined;
|
||||||
email: string;
|
email: string;
|
||||||
@@ -18,10 +15,16 @@ function classNames(...classes: string[]): string {
|
|||||||
export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
||||||
const { theme, switchTheme } = useTheme();
|
const { theme, switchTheme } = useTheme();
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
const db = createClient();
|
||||||
|
|
||||||
|
await db.auth.signOut();
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block w-full text-left">
|
<Menu as="div" className="relative inline-block w-full text-left">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="hover:bg-light-200 flex w-full items-center rounded-md p-1.5 text-neutral-900 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000">
|
<Menu.Button className="flex w-full items-center rounded-md p-1.5 text-neutral-900 hover:bg-light-200 dark:text-dark-900 dark:hover:bg-dark-200 dark:hover:text-dark-1000">
|
||||||
{imageUrl ? (
|
{imageUrl ? (
|
||||||
<Image
|
<Image
|
||||||
src={imageUrl ?? ""}
|
src={imageUrl ?? ""}
|
||||||
@@ -31,7 +34,7 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
alt=""
|
alt=""
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<span className="bg-light-400 inline-block h-6 w-6 overflow-hidden rounded-full dark:bg-dark-400">
|
<span className="inline-block h-6 w-6 overflow-hidden rounded-full bg-light-400 dark:bg-dark-400">
|
||||||
<svg
|
<svg
|
||||||
className="h-full w-full text-dark-700"
|
className="h-full w-full text-dark-700"
|
||||||
fill="currentColor"
|
fill="currentColor"
|
||||||
@@ -54,7 +57,7 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="border-light-600 bg-light-50 absolute bottom-[40px] left-0 z-10 mt-2 w-full origin-top-left rounded-md border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
<Menu.Items className="absolute bottom-[40px] left-0 z-10 mt-2 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
||||||
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
<div className="flex flex-col text-neutral-900 dark:text-dark-1000">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
<div className="flex w-full items-center px-3 py-2 text-left text-xs">
|
||||||
@@ -63,12 +66,12 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => switchTheme("system")}
|
onClick={() => switchTheme("system")}
|
||||||
className="hover:bg-light-200 flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
theme === "system" ? "visible" : "invisible",
|
theme === "system" ? "visible" : "invisible",
|
||||||
"bg-light-900 mr-4 h-[6px] w-[6px] rounded-full dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
System
|
System
|
||||||
@@ -77,12 +80,12 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => switchTheme("dark")}
|
onClick={() => switchTheme("dark")}
|
||||||
className="hover:bg-light-200 flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
theme === "dark" ? "visible" : "invisible",
|
theme === "dark" ? "visible" : "invisible",
|
||||||
"bg-light-900 mr-4 h-[6px] w-[6px] rounded-full dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
Dark
|
Dark
|
||||||
@@ -91,12 +94,12 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => switchTheme("light")}
|
onClick={() => switchTheme("light")}
|
||||||
className="hover:bg-light-200 flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs dark:hover:bg-dark-400"
|
className="flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
className={classNames(
|
className={classNames(
|
||||||
theme === "light" ? "visible" : "invisible",
|
theme === "light" ? "visible" : "invisible",
|
||||||
"bg-light-900 mr-4 h-[6px] w-[6px] rounded-full dark:bg-dark-900",
|
"mr-4 h-[6px] w-[6px] rounded-full bg-light-900 dark:bg-dark-900",
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
Light
|
Light
|
||||||
@@ -106,8 +109,8 @@ export default function UserMenu({ imageUrl, email }: UserMenuProps) {
|
|||||||
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
<div className="light-border-600 border-t-[1px] p-1 dark:border-dark-600">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => signOut({ callbackUrl: "/boards" })}
|
onClick={handleLogout}
|
||||||
className=" hover:bg-light-200 flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs dark:hover:bg-dark-400"
|
className=" flex w-full items-center rounded-[5px] px-3 py-2 text-left text-xs hover:bg-light-200 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
Logout
|
Logout
|
||||||
</button>
|
</button>
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
import { HiCheck } from "react-icons/hi2";
|
import { HiCheck } from "react-icons/hi2";
|
||||||
|
|
||||||
@@ -15,7 +13,7 @@ export default function WorkspaceMenu() {
|
|||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
<Menu as="div" className="relative inline-block w-full pb-3 text-left">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="hover:bg-light-200 mb-1 flex w-full items-center rounded-[5px] p-1.5 dark:hover:bg-dark-200">
|
<Menu.Button className="mb-1 flex w-full items-center rounded-[5px] p-1.5 hover:bg-light-200 dark:hover:bg-dark-200">
|
||||||
<span className="inline-flex h-6 w-6 items-center justify-center rounded-[5px] bg-indigo-700">
|
<span className="inline-flex h-6 w-6 items-center justify-center rounded-[5px] bg-indigo-700">
|
||||||
<span className="text-xs font-bold leading-none text-white">
|
<span className="text-xs font-bold leading-none text-white">
|
||||||
{workspace?.name.charAt(0).toUpperCase()}
|
{workspace?.name.charAt(0).toUpperCase()}
|
||||||
@@ -36,14 +34,14 @@ export default function WorkspaceMenu() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-600 absolute left-0 z-10 w-full origin-top-left rounded-md border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
<Menu.Items className="absolute left-0 z-10 w-full origin-top-left rounded-md border border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-600 dark:bg-dark-300">
|
||||||
<div className="p-1">
|
<div className="p-1">
|
||||||
{availableWorkspaces.map((availableWorkspace) => (
|
{availableWorkspaces.map((availableWorkspace) => (
|
||||||
<div key={availableWorkspace.publicId} className="flex">
|
<div key={availableWorkspace.publicId} className="flex">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => switchWorkspace(availableWorkspace)}
|
onClick={() => switchWorkspace(availableWorkspace)}
|
||||||
className="hover:bg-light-200 flex w-full items-center justify-between rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 dark:text-dark-1000 dark:hover:bg-dark-400"
|
className="flex w-full items-center justify-between 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"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<span className="inline-flex h-5 w-5 items-center justify-center rounded-[5px] bg-indigo-700">
|
<span className="inline-flex h-5 w-5 items-center justify-center rounded-[5px] bg-indigo-700">
|
||||||
@@ -65,11 +63,11 @@ export default function WorkspaceMenu() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="border-light-600 border-t-[1px] p-1 dark:border-dark-500">
|
<div className="border-t-[1px] border-light-600 p-1 dark:border-dark-500">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal("NEW_WORKSPACE")}
|
onClick={() => openModal("NEW_WORKSPACE")}
|
||||||
className="hover:bg-light-200 flex w-full items-center justify-between rounded-[5px] px-3 py-2 text-left text-xs text-neutral-900 dark:text-dark-1000 dark:hover:bg-dark-400"
|
className="flex w-full items-center justify-between rounded-[5px] px-3 py-2 text-left text-xs text-neutral-900 hover:bg-light-200 dark:text-dark-1000 dark:hover:bg-dark-400"
|
||||||
>
|
>
|
||||||
Create workspace
|
Create workspace
|
||||||
</button>
|
</button>
|
||||||
@@ -1,22 +1,28 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
import { signIn } from "next-auth/react";
|
import { api } from "~/utils/api";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Login() {
|
export function Login() {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const login = api.auth.login.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
router.push("/verify");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{
|
initialValues={{
|
||||||
email: "",
|
email: "",
|
||||||
}}
|
}}
|
||||||
onSubmit={async (values: FormValues) => {
|
onSubmit={(values: FormValues) => {
|
||||||
await signIn("email", {
|
login.mutate({
|
||||||
email: values.email,
|
email: values.email,
|
||||||
callbackUrl: "/boards",
|
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
22
src/components/dashboard.tsx
Normal file
22
src/components/dashboard.tsx
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
import SideNavigation from "./SideNavigation";
|
||||||
|
import FeedbackButton from "./FeedbackButton";
|
||||||
|
|
||||||
|
export default function Dashboard(props: { children: React.ReactNode }) {
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen flex-col items-center bg-light-100 dark:bg-dark-50">
|
||||||
|
<div className="m-auto flex h-16 w-full justify-between border-b border-light-600 px-5 py-2 align-middle dark:border-dark-600">
|
||||||
|
<div className="my-auto flex w-full items-center justify-between">
|
||||||
|
<h1 className="text-lg font-bold tracking-tight text-neutral-900 dark:text-dark-1000">
|
||||||
|
kan.bn
|
||||||
|
</h1>
|
||||||
|
<FeedbackButton />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-full w-full">
|
||||||
|
<SideNavigation user={{ email: "henry_ball@hotmail.co.uk" }} />
|
||||||
|
<div className="w-full overflow-hidden">{props.children}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Dialog, Transition } from "@headlessui/react";
|
import { Dialog, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -24,7 +22,7 @@ const Modal: React.FC<Props> = ({ children }) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<div className="bg-light-50 fixed inset-0 bg-opacity-40 transition-opacity dark:bg-dark-50 dark:bg-opacity-40" />
|
<div className="fixed inset-0 bg-light-50 bg-opacity-40 transition-opacity dark:bg-dark-50 dark:bg-opacity-40" />
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
|
|
||||||
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
<div className="fixed inset-0 z-10 w-screen overflow-y-auto">
|
||||||
@@ -38,7 +36,7 @@ const Modal: React.FC<Props> = ({ children }) => {
|
|||||||
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
leaveFrom="opacity-100 translate-y-0 sm:scale-100"
|
||||||
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
leaveTo="opacity-0 translate-y-4 sm:translate-y-0 sm:scale-95"
|
||||||
>
|
>
|
||||||
<Dialog.Panel className="shadow-3xl-light dark:shadow-3xl-dark bg-light-50 border-light-600 relative mt-[25vh] transform rounded-lg border px-4 pb-4 pt-5 text-left transition-all dark:border-dark-600 dark:bg-dark-100/20 dark:backdrop-blur-lg sm:mb-8 sm:w-full sm:max-w-sm sm:p-6">
|
<Dialog.Panel className="relative mt-[25vh] transform rounded-lg border border-light-600 bg-light-50 px-4 pb-4 pt-5 text-left shadow-3xl-light transition-all dark:border-dark-600 dark:bg-dark-100/20 dark:shadow-3xl-dark dark:backdrop-blur-lg sm:mb-8 sm:w-full sm:max-w-sm sm:p-6">
|
||||||
{children}
|
{children}
|
||||||
</Dialog.Panel>
|
</Dialog.Panel>
|
||||||
</Transition.Child>
|
</Transition.Child>
|
||||||
18
src/env.mjs
18
src/env.mjs
@@ -17,17 +17,6 @@ export const env = createEnv({
|
|||||||
NODE_ENV: z
|
NODE_ENV: z
|
||||||
.enum(["development", "test", "production"])
|
.enum(["development", "test", "production"])
|
||||||
.default("development"),
|
.default("development"),
|
||||||
NEXTAUTH_SECRET:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? z.string()
|
|
||||||
: z.string().optional(),
|
|
||||||
NEXTAUTH_URL: z.preprocess(
|
|
||||||
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
|
|
||||||
// Since NextAuth.js automatically uses the VERCEL_URL if present.
|
|
||||||
(str) => process.env.VERCEL_URL ?? str,
|
|
||||||
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
|
|
||||||
process.env.VERCEL ? z.string() : z.string().url(),
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -46,8 +35,7 @@ export const env = createEnv({
|
|||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
POSTGRES_URL: process.env.POSTGRES_URL,
|
POSTGRES_URL: process.env.POSTGRES_URL,
|
||||||
NODE_ENV: process.env.NODE_ENV,
|
NODE_ENV: process.env.NODE_ENV,
|
||||||
NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
|
// NEXT_PUBLIC_CLIENTVAR: process.env.NEXT_PUBLIC_CLIENTVAR,
|
||||||
NEXTAUTH_URL: process.env.NEXTAUTH_URL,
|
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
* Run `build` or `dev` with `SKIP_ENV_VALIDATION` to skip env validation. This is especially
|
||||||
@@ -55,8 +43,8 @@ export const env = createEnv({
|
|||||||
*/
|
*/
|
||||||
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
skipValidation: !!process.env.SKIP_ENV_VALIDATION,
|
||||||
/**
|
/**
|
||||||
* Makes it so that empty strings are treated as undefined.
|
* Makes it so that empty strings are treated as undefined. `SOME_VAR: z.string()` and
|
||||||
* `SOME_VAR: z.string()` and `SOME_VAR=''` will throw an error.
|
* `SOME_VAR=''` will throw an error.
|
||||||
*/
|
*/
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
});
|
});
|
||||||
|
|||||||
44
src/pages/_app.tsx
Normal file
44
src/pages/_app.tsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import "~/styles/globals.css";
|
||||||
|
|
||||||
|
import { type AppType } from "next/app";
|
||||||
|
import { Plus_Jakarta_Sans } from "next/font/google";
|
||||||
|
|
||||||
|
import { ModalProvider } from "~/providers/modal";
|
||||||
|
import { BoardProvider } from "~/providers/board";
|
||||||
|
import { ThemeProvider } from "~/providers/theme";
|
||||||
|
|
||||||
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
|
const jakarta = Plus_Jakarta_Sans({
|
||||||
|
subsets: ["latin"],
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Kan",
|
||||||
|
description: "The open source Trello alternative",
|
||||||
|
icons: [{ rel: "icon", url: "/favicon.ico" }],
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyApp: AppType = ({ Component, pageProps }) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style jsx global>{`
|
||||||
|
html {
|
||||||
|
font-family: ${jakarta.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<main className="h-screen overflow-hidden font-sans">
|
||||||
|
<ThemeProvider>
|
||||||
|
<ModalProvider>
|
||||||
|
<BoardProvider>
|
||||||
|
<Component {...pageProps} />
|
||||||
|
</BoardProvider>
|
||||||
|
</ModalProvider>
|
||||||
|
</ThemeProvider>
|
||||||
|
</main>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default api.withTRPC(MyApp);
|
||||||
43
src/pages/api/auth/confirm.ts
Normal file
43
src/pages/api/auth/confirm.ts
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
import { type EmailOtpType } from "@supabase/supabase-js";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { createNextClient } from "~/utils/supabase/api";
|
||||||
|
|
||||||
|
function stringOrFirstString(item: string | string[] | undefined) {
|
||||||
|
return Array.isArray(item) ? item[0] : item;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
res.status(405).appendHeader("Allow", "GET").end();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryParams = req.query;
|
||||||
|
const token_hash = stringOrFirstString(queryParams.token_hash);
|
||||||
|
const type = stringOrFirstString(queryParams.type);
|
||||||
|
|
||||||
|
let next = "/error";
|
||||||
|
|
||||||
|
if (token_hash && type) {
|
||||||
|
const supabase = createNextClient(req, res);
|
||||||
|
const { error } = await supabase.auth.verifyOtp({
|
||||||
|
type: type as EmailOtpType,
|
||||||
|
token_hash,
|
||||||
|
});
|
||||||
|
if (error) {
|
||||||
|
console.error(error);
|
||||||
|
} else {
|
||||||
|
next = stringOrFirstString(queryParams.next) ?? "/";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
res.redirect(next);
|
||||||
|
}
|
||||||
|
|
||||||
|
// export const runtime = "edge";
|
||||||
|
// export const preferredRegion = "lhr1";
|
||||||
|
// export const dynamic = "force-dynamic";
|
||||||
@@ -1,28 +1,28 @@
|
|||||||
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
|
||||||
import { type NextRequest } from "next/server";
|
import { type NextRequest } from "next/server";
|
||||||
|
import { fetchRequestHandler } from "@trpc/server/adapters/fetch";
|
||||||
|
|
||||||
import { env } from "~/env.mjs";
|
import { env } from "~/env.mjs";
|
||||||
import { appRouter } from "~/server/api/root";
|
import { appRouter } from "~/server/api/root";
|
||||||
import { createTRPCContext } from "~/server/api/trpc";
|
import { createTRPCContext } from "~/server/api/trpc";
|
||||||
|
|
||||||
const handler = (req: NextRequest) =>
|
export default async function handler(req: NextRequest) {
|
||||||
fetchRequestHandler({
|
return fetchRequestHandler({
|
||||||
endpoint: "/api/trpc",
|
endpoint: "/api/trpc",
|
||||||
req,
|
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
createContext: () => createTRPCContext({ req }),
|
req,
|
||||||
|
createContext: createTRPCContext,
|
||||||
onError:
|
onError:
|
||||||
env.NODE_ENV === "development"
|
env.NODE_ENV === "development"
|
||||||
? ({ path, error }) => {
|
? ({ path, error }) => {
|
||||||
console.error(
|
console.error(
|
||||||
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`
|
`❌ tRPC failed on ${path ?? "<no-path>"}: ${error.message}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export { handler as GET, handler as POST };
|
export const config = {
|
||||||
|
runtime: "edge",
|
||||||
export const runtime = "edge";
|
regions: ["lhr1"],
|
||||||
export const preferredRegion = 'lhr1';
|
};
|
||||||
export const dynamic = 'force-dynamic'
|
|
||||||
13
src/pages/boards/[...boardId]/index.tsx
Normal file
13
src/pages/boards/[...boardId]/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import BoardView from "~/views/board";
|
||||||
|
|
||||||
|
export default function BoardPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Dashboard>
|
||||||
|
<BoardView />
|
||||||
|
</Dashboard>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/pages/boards/index.tsx
Normal file
13
src/pages/boards/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import BoardsView from "~/views/boards";
|
||||||
|
|
||||||
|
export default function BoardsPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Dashboard>
|
||||||
|
<BoardsView />
|
||||||
|
</Dashboard>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/pages/cards/[cardId]/index.tsx
Normal file
13
src/pages/cards/[cardId]/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import CardView from "~/views/card";
|
||||||
|
|
||||||
|
export default function CardPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Dashboard>
|
||||||
|
<CardView />
|
||||||
|
</Dashboard>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/pages/login/index.tsx
Normal file
5
src/pages/login/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import LoginView from "~/views/auth/login";
|
||||||
|
|
||||||
|
export default function LoginPage() {
|
||||||
|
return <LoginView />;
|
||||||
|
}
|
||||||
13
src/pages/members/index.tsx
Normal file
13
src/pages/members/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import MembersView from "~/views/members";
|
||||||
|
|
||||||
|
export default function MembersPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Dashboard>
|
||||||
|
<MembersView />
|
||||||
|
</Dashboard>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/pages/settings/index.tsx
Normal file
13
src/pages/settings/index.tsx
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { WorkspaceProvider } from "~/providers/workspace";
|
||||||
|
import Dashboard from "~/components/dashboard";
|
||||||
|
import SettingsView from "~/views/settings";
|
||||||
|
|
||||||
|
export default function SettingsPage() {
|
||||||
|
return (
|
||||||
|
<WorkspaceProvider>
|
||||||
|
<Dashboard>
|
||||||
|
<SettingsView />
|
||||||
|
</Dashboard>
|
||||||
|
</WorkspaceProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
src/pages/verify/index.tsx
Normal file
5
src/pages/verify/index.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import VerifyView from "~/views/auth/verify";
|
||||||
|
|
||||||
|
export default function VerifyPage() {
|
||||||
|
return <VerifyView />;
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
@@ -7,7 +5,7 @@ import React, {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
interface BoardContextProps {
|
interface BoardContextProps {
|
||||||
boardData: BoardData;
|
boardData: BoardData;
|
||||||
@@ -23,7 +21,7 @@ interface BoardData {
|
|||||||
lists: List[];
|
lists: List[];
|
||||||
workspace: {
|
workspace: {
|
||||||
members: Members[];
|
members: Members[];
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Label {
|
interface Label {
|
||||||
@@ -44,7 +42,7 @@ interface Members {
|
|||||||
publicId: string;
|
publicId: string;
|
||||||
user: {
|
user: {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
};
|
} | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Card {
|
interface Card {
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { createContext, useContext, useState } from "react";
|
import { createContext, useContext, useState } from "react";
|
||||||
|
|
||||||
type ModalContextType = {
|
type ModalContextType = {
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import React, {
|
import React, {
|
||||||
createContext,
|
createContext,
|
||||||
useContext,
|
useContext,
|
||||||
@@ -8,7 +6,7 @@ import React, {
|
|||||||
type ReactNode,
|
type ReactNode,
|
||||||
} from "react";
|
} from "react";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
@@ -60,21 +58,27 @@ export const WorkspaceProvider: React.FC<{ children: ReactNode }> = ({
|
|||||||
localStorage.getItem("workspacePublicId");
|
localStorage.getItem("workspacePublicId");
|
||||||
|
|
||||||
if (data?.length) {
|
if (data?.length) {
|
||||||
const workspaces = data.map(({ workspace }) => ({
|
const workspaces = data
|
||||||
publicId: workspace.publicId,
|
.map(({ workspace }) => {
|
||||||
name: workspace.name,
|
if (!workspace) return;
|
||||||
}));
|
|
||||||
|
|
||||||
setAvailableWorkspaces(workspaces);
|
return {
|
||||||
|
publicId: workspace.publicId,
|
||||||
|
name: workspace.name,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter((workspace) => workspace !== null) as Workspace[];
|
||||||
|
|
||||||
|
if (workspaces.length) setAvailableWorkspaces(workspaces);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (storedWorkspaceId !== null) {
|
if (storedWorkspaceId !== null) {
|
||||||
const newData = data;
|
const newData = data;
|
||||||
const selectedWorkspace = newData?.find(
|
const selectedWorkspace = newData?.find(
|
||||||
({ workspace }) => workspace.publicId === storedWorkspaceId,
|
({ workspace }) => workspace?.publicId === storedWorkspaceId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!selectedWorkspace) return;
|
if (!selectedWorkspace?.workspace) return;
|
||||||
|
|
||||||
setWorkspace({
|
setWorkspace({
|
||||||
publicId: selectedWorkspace.workspace.publicId,
|
publicId: selectedWorkspace.workspace.publicId,
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { authRouter } from "~/server/api/routers/auth";
|
||||||
import { boardRouter } from "~/server/api/routers/board";
|
import { boardRouter } from "~/server/api/routers/board";
|
||||||
import { cardRouter } from "~/server/api/routers/card";
|
import { cardRouter } from "~/server/api/routers/card";
|
||||||
import { labelRouter } from "~/server/api/routers/label";
|
import { labelRouter } from "~/server/api/routers/label";
|
||||||
@@ -12,6 +13,7 @@ import { createTRPCRouter } from "~/server/api/trpc";
|
|||||||
* All routers added in /api/routers should be manually added here.
|
* All routers added in /api/routers should be manually added here.
|
||||||
*/
|
*/
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
|
auth: authRouter,
|
||||||
board: boardRouter,
|
board: boardRouter,
|
||||||
card: cardRouter,
|
card: cardRouter,
|
||||||
label: labelRouter,
|
label: labelRouter,
|
||||||
@@ -22,3 +24,13 @@ export const appRouter = createTRPCRouter({
|
|||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
export type AppRouter = typeof appRouter;
|
export type AppRouter = typeof appRouter;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a server-side caller for the tRPC API.
|
||||||
|
* @example
|
||||||
|
* const trpc = createCaller(createContext);
|
||||||
|
* const res = await trpc.post.all();
|
||||||
|
* ^? Post[]
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||||
|
// export const createCaller = createCallerFactory(appRouter);
|
||||||
|
|||||||
18
src/server/api/routers/auth.ts
Normal file
18
src/server/api/routers/auth.ts
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { createTRPCRouter, publicProcedure } from "~/server/api/trpc";
|
||||||
|
|
||||||
|
export const authRouter = createTRPCRouter({
|
||||||
|
login: publicProcedure
|
||||||
|
.input(z.object({ email: z.string() }))
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const { data } = await ctx.db.auth.signInWithOtp({
|
||||||
|
email: input.email,
|
||||||
|
options: {
|
||||||
|
emailRedirectTo: `${process.env.WEBSITE_URL}/boards`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1,203 +1,177 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { and, eq, asc, isNull, inArray } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { boards, cards, lists, workspaces } from "~/server/db/schema";
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const boardRouter = createTRPCRouter({
|
export const boardRouter = createTRPCRouter({
|
||||||
all: publicProcedure
|
all: protectedProcedure
|
||||||
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
.input(z.object({ workspacePublicId: z.string().min(12) }))
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const workspace = await ctx.db
|
||||||
|
.from("workspace")
|
||||||
|
.select(`id`)
|
||||||
|
.eq("publicId", input.workspacePublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
// @todo: validate user has access to workspace
|
if (!workspace.data) return;
|
||||||
|
|
||||||
if (!userId) return;
|
const { data } = await ctx.db
|
||||||
|
.from("board")
|
||||||
|
.select(`publicId, name`)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.eq("workspaceId", workspace.data.id);
|
||||||
|
|
||||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
return data;
|
||||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!workspace) return;
|
|
||||||
|
|
||||||
return ctx.db.query.boards.findMany({
|
|
||||||
where: and(eq(boards.workspaceId, workspace.id), isNull(boards.deletedAt)),
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
byId: publicProcedure
|
byId: protectedProcedure
|
||||||
.input(z.object({ id: z.string().min(12) }))
|
.input(z.object({ id: z.string().min(12) }))
|
||||||
.query(({ ctx, input }) =>
|
.query(async ({ ctx, input }) => {
|
||||||
ctx.db.query.boards.findFirst({
|
const { data } = await ctx.db
|
||||||
where: and(eq(boards.publicId, input.id), isNull(boards.deletedAt)),
|
.from("board")
|
||||||
columns: {
|
.select(
|
||||||
publicId: true,
|
`
|
||||||
name: true,
|
publicId,
|
||||||
},
|
name,
|
||||||
with: {
|
workspace (
|
||||||
workspace: {
|
publicId,
|
||||||
columns: {
|
members:workspace_members (
|
||||||
publicId: true,
|
publicId,
|
||||||
},
|
user (
|
||||||
with: {
|
name
|
||||||
members: {
|
)
|
||||||
columns: {
|
)
|
||||||
publicId: true,
|
),
|
||||||
},
|
labels:label (
|
||||||
with: {
|
publicId,
|
||||||
user: {
|
name,
|
||||||
columns: {
|
colourCode
|
||||||
name: true
|
),
|
||||||
}
|
lists:list (
|
||||||
}
|
publicId,
|
||||||
}
|
name,
|
||||||
}
|
boardId,
|
||||||
}
|
index,
|
||||||
},
|
cards:card (
|
||||||
labels: {
|
publicId,
|
||||||
columns: {
|
title,
|
||||||
publicId: true,
|
description,
|
||||||
colourCode: true,
|
listId,
|
||||||
name: true,
|
index,
|
||||||
}
|
labels:label (
|
||||||
},
|
publicId,
|
||||||
lists: {
|
name,
|
||||||
orderBy: [asc(lists.index)],
|
colourCode
|
||||||
where: isNull(cards.deletedAt),
|
),
|
||||||
columns: {
|
members:workspace_members (
|
||||||
publicId: true,
|
publicId,
|
||||||
name: true,
|
user (
|
||||||
boardId: true,
|
name
|
||||||
index: true,
|
)
|
||||||
},
|
)
|
||||||
with: {
|
)
|
||||||
cards: {
|
)
|
||||||
where: isNull(cards.deletedAt),
|
`,
|
||||||
orderBy: [asc(cards.index)],
|
)
|
||||||
columns: {
|
.eq("publicId", input.id)
|
||||||
publicId: true,
|
.is("deletedAt", null)
|
||||||
title: true,
|
.is("lists.deletedAt", null)
|
||||||
description: true,
|
.is("lists.cards.deletedAt", null)
|
||||||
listId: true,
|
.order("index", { foreignTable: "list", ascending: true })
|
||||||
index: true,
|
.order("index", { foreignTable: "list.card", ascending: true })
|
||||||
},
|
.limit(1)
|
||||||
with: {
|
.single();
|
||||||
labels: {
|
|
||||||
columns: {
|
return data;
|
||||||
labelId: false,
|
}),
|
||||||
cardId: false,
|
create: protectedProcedure
|
||||||
},
|
|
||||||
with: {
|
|
||||||
label: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
colourCode: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
columns: {
|
|
||||||
workspaceMemberId: false,
|
|
||||||
cardId: false,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
member: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
columns: {
|
|
||||||
name: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
),
|
|
||||||
create: publicProcedure
|
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
workspacePublicId: z.string().min(12)
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
const workspace = await ctx.db
|
||||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
.from("workspace")
|
||||||
})
|
.select(`id`)
|
||||||
|
.eq("publicId", input.workspacePublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
if (!workspace) return;
|
if (!workspace.data) return;
|
||||||
|
|
||||||
return ctx.db.insert(boards).values({
|
const { data } = await ctx.db
|
||||||
publicId: generateUID(),
|
.from("board")
|
||||||
name: input.name,
|
.insert({
|
||||||
createdBy: userId,
|
publicId: generateUID(),
|
||||||
workspaceId: workspace.id
|
name: input.name,
|
||||||
});
|
createdBy: userId,
|
||||||
}),
|
workspaceId: workspace.data.id,
|
||||||
update: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
boardId: z.string().min(12),
|
|
||||||
name: z.string().min(1),
|
|
||||||
}))
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
return ctx.db.update(boards).set({ name: input.name }).where(eq(boards.publicId, input.boardId));
|
|
||||||
}),
|
|
||||||
delete: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
boardPublicId: z.string().min(12),
|
|
||||||
}))
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
|
||||||
const board = await tx.query.boards.findFirst({
|
|
||||||
where: eq(boards.publicId, input.boardPublicId),
|
|
||||||
with: {
|
|
||||||
lists: true,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!board) return;
|
|
||||||
|
|
||||||
const listIds = board.lists.map((list) => list.id)
|
|
||||||
|
|
||||||
await tx.update(boards).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(boards.id, board.id));
|
|
||||||
|
|
||||||
if (listIds.length) {
|
|
||||||
await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId }).where(eq(lists.boardId, board.id));
|
|
||||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId }).where(inArray(cards.listId, listIds));
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
|
.select(`publicId, name`);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
boardId: z.string().min(12),
|
||||||
|
name: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const { data } = await ctx.db
|
||||||
|
.from("board")
|
||||||
|
.update({ name: input.name })
|
||||||
|
.eq("publicId", input.boardId);
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
delete: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
boardPublicId: z.string().min(12),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
|
const board = await ctx.db
|
||||||
|
.from("board")
|
||||||
|
.select(`id, lists:list (id)`)
|
||||||
|
.eq("publicId", input.boardPublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!board.data) return;
|
||||||
|
|
||||||
|
const listIds = board.data.lists.map((list) => list.id);
|
||||||
|
|
||||||
|
const deletedAt = new Date().toISOString();
|
||||||
|
|
||||||
|
await ctx.db
|
||||||
|
.from("board")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.eq("id", board.data.id)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
|
||||||
|
if (listIds.length) {
|
||||||
|
await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.eq("boardId", board.data.id)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
|
||||||
|
await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.in("listId", listIds)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
}
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,358 +1,364 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { and, desc, eq, isNull, inArray, sql } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { cards, cardsToLabels, cardToWorkspaceMembers, labels, lists, workspaceMembers } from "~/server/db/schema";
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const cardRouter = createTRPCRouter({
|
export const cardRouter = createTRPCRouter({
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
title: z.string().min(1),
|
title: z.string().min(1),
|
||||||
listPublicId: z.string().min(12),
|
listPublicId: z.string().min(12),
|
||||||
labelsPublicIds: z.array(z.string().min(12)),
|
labelsPublicIds: z.array(z.string().min(12)),
|
||||||
memberPublicIds: z.array(z.string().min(12))
|
memberPublicIds: z.array(z.string().min(12)),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const list = await ctx.db
|
||||||
const list = await tx.query.lists.findFirst({
|
.from("list")
|
||||||
where: eq(lists.publicId, input.listPublicId),
|
.select(`id, cards:card (index)`)
|
||||||
columns: {
|
.eq("publicId", input.listPublicId)
|
||||||
id: true
|
.order("index", { foreignTable: "card", ascending: false })
|
||||||
}
|
.limit(1)
|
||||||
});
|
.single();
|
||||||
|
|
||||||
if (!list) return;
|
if (!list.data?.id) return;
|
||||||
|
|
||||||
const latestCard = await tx.query.cards.findFirst({
|
const latestCard = list.data.cards.length && list.data.cards[0];
|
||||||
where: and(eq(cards.listId, list.id), isNull(cards.deletedAt)),
|
|
||||||
columns: {
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
orderBy: desc(cards.index)
|
|
||||||
});
|
|
||||||
|
|
||||||
const newCard = await tx.insert(cards).values({
|
const newCard = await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.insert({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
title: input.title,
|
title: input.title,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: list.id,
|
listId: list.data.id,
|
||||||
index: latestCard ? latestCard.index + 1 : 0
|
index: latestCard ? latestCard.index + 1 : 0,
|
||||||
}).returning({ id: cards.id });
|
})
|
||||||
|
.select(`id`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const newCardId = newCard[0]?.id;
|
const newCardId = newCard.data?.id;
|
||||||
|
|
||||||
if (newCardId && input.labelsPublicIds.length) {
|
if (newCardId && input.labelsPublicIds.length) {
|
||||||
const labels = await tx.query.labels.findMany({
|
const labels = await ctx.db
|
||||||
where: inArray(cards.publicId, input.labelsPublicIds),
|
.from("label")
|
||||||
});
|
.select(`id`)
|
||||||
|
.eq("publicId", input.labelsPublicIds);
|
||||||
|
|
||||||
if (!labels.length) return;
|
if (!labels.data?.length) return;
|
||||||
|
|
||||||
const labelsInsert = labels.map((label) => ({ cardId: newCardId, labelId: label.id }))
|
const labelsInsert = labels.data.map((label) => ({
|
||||||
|
cardId: newCardId,
|
||||||
|
labelId: label.id,
|
||||||
|
}));
|
||||||
|
|
||||||
await tx.insert(cardsToLabels).values(labelsInsert);
|
await ctx.db.from("_card_labels").insert(labelsInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newCardId && input.memberPublicIds.length) {
|
if (newCardId && input.memberPublicIds.length) {
|
||||||
const members = await tx.query.workspaceMembers.findMany({
|
const members = await ctx.db
|
||||||
where: inArray(workspaceMembers.publicId, input.memberPublicIds),
|
.from("workspace_members")
|
||||||
});
|
.select(`id`)
|
||||||
|
.eq("publicId", input.memberPublicIds);
|
||||||
|
|
||||||
if (!members.length) return;
|
if (!members.data?.length) return;
|
||||||
|
|
||||||
const membersInsert = members.map((member) => ({ cardId: newCardId, workspaceMemberId: member.id}))
|
const membersInsert = members.data.map((member) => ({
|
||||||
|
cardId: newCardId,
|
||||||
|
workspaceMemberId: member.id,
|
||||||
|
}));
|
||||||
|
|
||||||
await tx.insert(cardToWorkspaceMembers).values(membersInsert);
|
await ctx.db.from("_card_workspace_members").insert(membersInsert);
|
||||||
}
|
}
|
||||||
|
|
||||||
return newCard;
|
return newCard;
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
addOrRemoveLabel: publicProcedure
|
addOrRemoveLabel: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
cardPublicId: z.string().min(12),
|
cardPublicId: z.string().min(12),
|
||||||
labelPublicId: z.string().min(12),
|
labelPublicId: z.string().min(12),
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
|
||||||
const card = await tx.query.cards.findFirst({
|
|
||||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
|
||||||
});
|
|
||||||
|
|
||||||
const label = await tx.query.labels.findFirst({
|
|
||||||
where: eq(labels.publicId, input.labelPublicId),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!card || !label) return;
|
|
||||||
|
|
||||||
const labelExists = await tx.query.cardsToLabels.findFirst({
|
|
||||||
where: and(eq(cardsToLabels.cardId, card.id), eq(cardsToLabels.labelId, label.id)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (labelExists) {
|
|
||||||
return tx.delete(cardsToLabels).where(and(eq(cardsToLabels.cardId, card.id), eq(cardsToLabels.labelId, label.id)),);
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.insert(cardsToLabels).values({
|
|
||||||
cardId: card.id,
|
|
||||||
labelId: label.id,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
addOrRemoveMember: publicProcedure
|
)
|
||||||
.input(
|
.mutation(async ({ ctx, input }) => {
|
||||||
z.object({
|
const userId = ctx.user?.id;
|
||||||
cardPublicId: z.string().min(12),
|
|
||||||
workspaceMemberPublicId: z.string().min(12),
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const card = await ctx.db
|
||||||
const card = await tx.query.cards.findFirst({
|
.from("card")
|
||||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
.select(`id`)
|
||||||
});
|
.eq("publicId", input.cardPublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const member = await tx.query.workspaceMembers.findFirst({
|
const label = await ctx.db
|
||||||
where: eq(workspaceMembers.publicId, input.workspaceMemberPublicId),
|
.from("label")
|
||||||
});
|
.select(`id`)
|
||||||
|
.eq("publicId", input.labelPublicId)
|
||||||
if (!card || !member) return;
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const memberExists = await tx.query.cardToWorkspaceMembers.findFirst({
|
if (!card.data || !label.data) return;
|
||||||
where: and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (memberExists) {
|
const existingLabel = await ctx.db
|
||||||
return tx.delete(cardToWorkspaceMembers).where(and(eq(cardToWorkspaceMembers.cardId, card.id), eq(cardToWorkspaceMembers.workspaceMemberId, member.id)),);
|
.from("_card_labels")
|
||||||
}
|
.select()
|
||||||
|
.eq("cardId", card.data.id)
|
||||||
|
.eq("labelId", label.data.id)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
return tx.insert(cardToWorkspaceMembers).values({
|
if (existingLabel.data) {
|
||||||
cardId: card.id,
|
await ctx.db
|
||||||
workspaceMemberId: member.id,
|
.from("_card_labels")
|
||||||
});
|
.delete()
|
||||||
})
|
.eq("cardId", card.data.id)
|
||||||
|
.eq("labelId", label.data.id);
|
||||||
|
|
||||||
|
return { newLabel: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.from("_card_labels").insert({
|
||||||
|
cardId: card.data.id,
|
||||||
|
labelId: label.data.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { newLabel: true };
|
||||||
|
}),
|
||||||
|
addOrRemoveMember: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cardPublicId: z.string().min(12),
|
||||||
|
workspaceMemberPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
byId: publicProcedure
|
)
|
||||||
.input(z.object({ id: z.string().min(12) }))
|
.mutation(async ({ ctx, input }) => {
|
||||||
.query(({ ctx, input }) =>
|
const userId = ctx.user?.id;
|
||||||
ctx.db.query.cards.findFirst({
|
|
||||||
with: {
|
|
||||||
labels: {
|
|
||||||
columns: {
|
|
||||||
labelId: false,
|
|
||||||
cardId: false,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
label: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
colourCode: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
list: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
board: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
labels: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
colourCode: true,
|
|
||||||
name: true,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
lists: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
where: isNull(lists.deletedAt)
|
|
||||||
},
|
|
||||||
workspace: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
members: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
members: {
|
|
||||||
columns: {
|
|
||||||
workspaceMemberId: false,
|
|
||||||
cardId: false,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
member: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: and(eq(cards.publicId, input.id), isNull(cards.deletedAt)),
|
|
||||||
})
|
|
||||||
),
|
|
||||||
update: publicProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
cardId: z.string().min(12),
|
|
||||||
title: z.string().min(1),
|
|
||||||
description: z.string(),
|
|
||||||
}))
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.update(cards).set({ title: input.title, description: input.description }).where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)));
|
const card = await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.select(`id`)
|
||||||
|
.eq("publicId", input.cardPublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
const member = await ctx.db
|
||||||
|
.from("workspace_members")
|
||||||
|
.select(`id`)
|
||||||
|
.eq("publicId", input.workspaceMemberPublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!card.data || !member.data) return;
|
||||||
|
|
||||||
|
const existingMember = await ctx.db
|
||||||
|
.from("_card_workspace_members")
|
||||||
|
.select()
|
||||||
|
.eq("cardId", card.data.id)
|
||||||
|
.eq("workspaceMemberId", member.data.id)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (existingMember.data) {
|
||||||
|
await ctx.db
|
||||||
|
.from("_card_workspace_members")
|
||||||
|
.delete()
|
||||||
|
.eq("cardId", card.data.id)
|
||||||
|
.eq("workspaceMemberId", member.data.id);
|
||||||
|
|
||||||
|
return { newMember: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
await ctx.db.from("_card_workspace_members").insert({
|
||||||
|
cardId: card.data.id,
|
||||||
|
workspaceMemberId: member.data.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { newMember: true };
|
||||||
|
}),
|
||||||
|
byId: protectedProcedure
|
||||||
|
.input(z.object({ id: z.string().min(12) }))
|
||||||
|
.query(async ({ ctx, input }) => {
|
||||||
|
const { data } = await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
publicId,
|
||||||
|
title,
|
||||||
|
description,
|
||||||
|
labels:label (
|
||||||
|
publicId,
|
||||||
|
name,
|
||||||
|
colourCode
|
||||||
|
),
|
||||||
|
list (
|
||||||
|
publicId,
|
||||||
|
name,
|
||||||
|
board (
|
||||||
|
publicId,
|
||||||
|
name,
|
||||||
|
labels:label (
|
||||||
|
publicId,
|
||||||
|
colourCode,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
lists:list (
|
||||||
|
publicId,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
workspace (
|
||||||
|
publicId,
|
||||||
|
members:workspace_members (
|
||||||
|
publicId,
|
||||||
|
user (
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
members:workspace_members (
|
||||||
|
publicId,
|
||||||
|
user (
|
||||||
|
id,
|
||||||
|
name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.eq("publicId", input.id)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.is("list.board.lists.deletedAt", null)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}),
|
||||||
|
update: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cardId: z.string().min(12),
|
||||||
|
title: z.string().min(1),
|
||||||
|
description: z.string(),
|
||||||
}),
|
}),
|
||||||
delete: publicProcedure
|
)
|
||||||
.input(
|
.mutation(async ({ ctx, input }) => {
|
||||||
z.object({
|
const userId = ctx.user?.id;
|
||||||
cardPublicId: z.string().min(12),
|
|
||||||
}))
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const { data } = await ctx.db
|
||||||
const card = await tx.query.cards.findFirst({
|
.from("card")
|
||||||
where: eq(cards.publicId, input.cardPublicId),
|
.update({ title: input.title, description: input.description })
|
||||||
})
|
.eq("publicId", input.cardId)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
|
||||||
if (!card) return;
|
return data;
|
||||||
|
}),
|
||||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.publicId, input.cardPublicId));
|
delete: protectedProcedure
|
||||||
|
.input(
|
||||||
await tx.execute(sql`UPDATE ${cards} SET ${cards.index} = ${cards.index} - 1 WHERE ${cards.listId} = ${card.listId} AND ${cards.index} > ${card.index} AND ${cards.deletedAt} IS NULL;`);
|
z.object({
|
||||||
})
|
cardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
reorder: publicProcedure
|
)
|
||||||
.input(
|
.mutation(async ({ ctx, input }) => {
|
||||||
z.object({
|
const userId = ctx.user?.id;
|
||||||
cardId: z.string().min(12),
|
|
||||||
newListId: z.string().min(12),
|
|
||||||
newIndex: z.number().optional(),
|
|
||||||
}))
|
|
||||||
.mutation(({ ctx, input }) => {
|
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const card = await ctx.db
|
||||||
const card = await tx.query.cards.findFirst({
|
.from("card")
|
||||||
with: {
|
.select(`id, index, listId`)
|
||||||
list: true,
|
.eq("publicId", input.cardPublicId)
|
||||||
},
|
.limit(1)
|
||||||
where: and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)),
|
.single();
|
||||||
});
|
|
||||||
|
|
||||||
if (!card) return;
|
if (!card.data) return;
|
||||||
|
|
||||||
const currentList = card.list;
|
const deletedAt = new Date().toISOString();
|
||||||
const currentIndex = card.index;
|
|
||||||
|
|
||||||
let newIndex = input.newIndex;
|
|
||||||
|
|
||||||
const newList = await tx.query.lists.findFirst({
|
await ctx.db
|
||||||
with: {
|
.from("card")
|
||||||
cards: {
|
.update({ deletedAt, deletedBy: userId })
|
||||||
orderBy: [desc(cards.index)],
|
.eq("publicId", input.cardPublicId);
|
||||||
limit: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
where: and(eq(lists.publicId, input.newListId), isNull(cards.deletedAt)),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!newList) return;
|
await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.eq("publicId", input.cardPublicId);
|
||||||
|
|
||||||
if (newIndex === undefined) {
|
await ctx.db.rpc("shift_card_index", {
|
||||||
const lastCardIndex = newList.cards.length ? newList.cards[0]?.index : undefined;
|
list_id: card.data.listId,
|
||||||
|
card_index: card.data.index,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
reorder: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
cardId: z.string().min(12),
|
||||||
|
newListId: z.string().min(12),
|
||||||
|
newIndex: z.number().optional(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
if (!userId) return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!currentList?.id || !newList?.id) return;
|
const card = await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.select(`id, index, list (id)`)
|
||||||
|
.eq("publicId", input.cardId)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!card.data) return;
|
||||||
|
|
||||||
if (currentList.id === newList.id) {
|
const currentList = card.data.list;
|
||||||
await tx.execute(sql`
|
const currentIndex = card.data.index;
|
||||||
UPDATE ${cards}
|
|
||||||
SET index =
|
|
||||||
CASE
|
|
||||||
WHEN ${cards.index} = ${currentIndex} THEN ${newIndex}
|
|
||||||
WHEN ${currentIndex} < ${newIndex} AND ${cards.index} > ${currentIndex} AND ${cards.index} <= ${newIndex} THEN ${cards.index} - 1
|
|
||||||
WHEN ${currentIndex} > ${newIndex} AND ${cards.index} >= ${newIndex} AND ${cards.index} < ${currentIndex} THEN ${cards.index} + 1
|
|
||||||
ELSE ${cards.index}
|
|
||||||
END
|
|
||||||
WHERE ${cards.listId} = ${currentList.id} AND ${cards.deletedAt} IS NULL;
|
|
||||||
`);
|
|
||||||
} else {
|
|
||||||
await tx.execute(sql`UPDATE ${cards} SET index = index + 1 WHERE ${cards.listId} = ${newList.id} AND ${cards.index} >= ${newIndex} AND ${cards.deletedAt} IS NULL;`)
|
|
||||||
|
|
||||||
await tx.execute(sql`UPDATE ${cards} SET index = index - 1 WHERE ${cards.listId} = ${currentList.id} AND ${cards.index} >= ${currentIndex} AND ${cards.deletedAt} IS NULL;`)
|
let newIndex = input.newIndex;
|
||||||
|
|
||||||
await tx
|
const newList = await ctx.db
|
||||||
.update(cards)
|
.from("list")
|
||||||
.set({ listId: newList.id, index: newIndex })
|
.select(`id, cards:card (index)`)
|
||||||
.where(and(eq(cards.publicId, input.cardId), isNull(cards.deletedAt)));
|
.eq("publicId", input.newListId)
|
||||||
}
|
.is("deletedAt", null)
|
||||||
})
|
.order("index", { foreignTable: "card", ascending: false })
|
||||||
})
|
.limit(1)
|
||||||
});
|
.single();
|
||||||
|
|
||||||
|
if (!newList.data) return;
|
||||||
|
|
||||||
|
if (newIndex === undefined) {
|
||||||
|
const lastCardIndex = newList.data.cards.length
|
||||||
|
? newList.data.cards[0]?.index
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
newIndex = lastCardIndex !== undefined ? lastCardIndex + 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!currentList?.id || !newList?.data.id) return;
|
||||||
|
|
||||||
|
const { error } = await ctx.db.rpc("reorder_cards", {
|
||||||
|
current_list_id: currentList.id,
|
||||||
|
new_list_id: newList.data.id,
|
||||||
|
current_index: currentIndex,
|
||||||
|
new_index: newIndex,
|
||||||
|
card_id: card.data.id,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { success: !!error };
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { eq } from "drizzle-orm";
|
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
import { boards, cards, imports, lists, workspaces } from "~/server/db/schema";
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
const TRELLO_API_URL = 'https://api.trello.com/1';
|
const TRELLO_API_URL = "https://api.trello.com/1";
|
||||||
|
|
||||||
interface TrelloBoard {
|
interface TrelloBoard {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -36,29 +31,29 @@ interface MemberData {
|
|||||||
|
|
||||||
export const importRouter = createTRPCRouter({
|
export const importRouter = createTRPCRouter({
|
||||||
trello: createTRPCRouter({
|
trello: createTRPCRouter({
|
||||||
getBoards: publicProcedure
|
getBoards: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
apiKey: z.string().length(32),
|
apiKey: z.string().length(32),
|
||||||
token: z.string().length(76),
|
token: z.string().length(76),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ ctx, input }) => {
|
.query(async ({ input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const fetchMemberRes = await fetch(
|
||||||
|
`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`,
|
||||||
if (!userId) return;
|
);
|
||||||
|
|
||||||
const fetchMemberRes = await fetch(`${TRELLO_API_URL}/tokens/${input.token}/member?key=${input.apiKey}`)
|
const member = (await fetchMemberRes.json()) as MemberData;
|
||||||
|
|
||||||
const member = await fetchMemberRes.json() as MemberData;
|
|
||||||
|
|
||||||
const boardIds = member.idBoards;
|
const boardIds = member.idBoards;
|
||||||
|
|
||||||
const fetchBoard = async (boardId: string) => {
|
const fetchBoard = async (boardId: string) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`);
|
const response = await fetch(
|
||||||
const data = await response.json() as TrelloBoard;
|
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}`,
|
||||||
|
);
|
||||||
|
const data = (await response.json()) as TrelloBoard;
|
||||||
|
|
||||||
return data;
|
return data;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -72,37 +67,57 @@ export const importRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const boardDataArray = await Promise.all(boards);
|
const boardDataArray = await Promise.all(boards);
|
||||||
|
|
||||||
return boardDataArray.map((board) => ({ id: board.id, name: board.name }));
|
return boardDataArray.map((board) => ({
|
||||||
|
id: board.id,
|
||||||
|
name: board.name,
|
||||||
|
}));
|
||||||
}),
|
}),
|
||||||
importBoards: publicProcedure
|
importBoards: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardIds: z.array(z.string()),
|
boardIds: z.array(z.string()),
|
||||||
apiKey: z.string().length(32),
|
apiKey: z.string().length(32),
|
||||||
token: z.string().length(76),
|
token: z.string().length(76),
|
||||||
workspacePublicId: z.string().min(12)
|
workspacePublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
const newImport = await ctx.db.insert(imports).values({
|
const newImport = await ctx.db
|
||||||
publicId: generateUID(),
|
.from("import")
|
||||||
source: 'trello',
|
.insert({
|
||||||
createdBy: userId,
|
publicId: generateUID(),
|
||||||
status: 'started'
|
source: "trello",
|
||||||
}).returning({ id: imports.id });
|
createdBy: userId,
|
||||||
|
status: "started",
|
||||||
|
})
|
||||||
|
.select(`id`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const newImportId = newImport[0]?.id;
|
const newImportId = newImport.data?.id;
|
||||||
|
|
||||||
let boardsCreated = 0;
|
let boardsCreated = 0;
|
||||||
|
|
||||||
|
const workspace = await ctx.db
|
||||||
|
.from("workspace")
|
||||||
|
.select(`id`)
|
||||||
|
.eq("publicId", input.workspacePublicId)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
|
if (!workspace.data) return;
|
||||||
|
|
||||||
for (const boardId of input.boardIds) {
|
for (const boardId of input.boardIds) {
|
||||||
const response = await fetch(`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`);
|
const response = await fetch(
|
||||||
const data = await response.json() as TrelloBoard;
|
`${TRELLO_API_URL}/boards/${boardId}?key=${input.apiKey}&token=${input.token}&lists=open&cards=open`,
|
||||||
|
);
|
||||||
|
const data = (await response.json()) as TrelloBoard;
|
||||||
|
|
||||||
const formattedData = {
|
const formattedData = {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -112,72 +127,74 @@ export const importRouter = createTRPCRouter({
|
|||||||
.filter((card) => card.idList === list.id)
|
.filter((card) => card.idList === list.id)
|
||||||
.map((_card) => ({
|
.map((_card) => ({
|
||||||
name: _card.name,
|
name: _card.name,
|
||||||
description: _card.desc
|
description: _card.desc,
|
||||||
}))
|
})),
|
||||||
}))
|
})),
|
||||||
}
|
};
|
||||||
|
|
||||||
const workspace = await ctx.db.query.workspaces.findFirst({
|
const newBoard = await ctx.db
|
||||||
where: eq(workspaces.publicId, input.workspacePublicId),
|
.from("board")
|
||||||
})
|
.insert({
|
||||||
|
|
||||||
if (!workspace) return;
|
|
||||||
|
|
||||||
await ctx.db.transaction(async (tx) => {
|
|
||||||
const newBoard = await tx.insert(boards).values({
|
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: data.name,
|
name: data.name,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
importId: newImportId,
|
importId: newImportId,
|
||||||
workspaceId: workspace.id
|
workspaceId: workspace.data.id,
|
||||||
}).returning({ id: boards.id });
|
})
|
||||||
|
.select(`id`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const newBoardId = newBoard[0]?.id
|
const newBoardId = newBoard.data?.id;
|
||||||
|
|
||||||
if (!newBoardId) return;
|
if (!newBoardId) return;
|
||||||
|
|
||||||
let listIndex = 0;
|
let listIndex = 0;
|
||||||
|
|
||||||
for (const list of formattedData.lists) {
|
for (const list of formattedData.lists) {
|
||||||
const newList = await tx.insert(lists).values({
|
const newList = await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.insert({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: list.name,
|
name: list.name,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
boardId: newBoardId,
|
boardId: newBoardId,
|
||||||
index: listIndex,
|
index: listIndex,
|
||||||
importId: newImportId
|
importId: newImportId,
|
||||||
}).returning({ id: lists.id });
|
})
|
||||||
|
.select(`id`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const newListId = newList[0]?.id
|
const newListId = newList.data?.id;
|
||||||
|
|
||||||
if (list.cards.length && newListId) {
|
if (list.cards.length && newListId) {
|
||||||
const cardsInsert = list.cards.map((card, index) => ({
|
const cardsInsert = list.cards.map((card, index) => ({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
title: card.name,
|
title: card.name,
|
||||||
description: card.description,
|
description: card.description,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
listId: newListId,
|
listId: newListId,
|
||||||
index,
|
index,
|
||||||
importId: newImportId
|
importId: newImportId,
|
||||||
}))
|
}));
|
||||||
|
|
||||||
await tx.insert(cards).values(cardsInsert);
|
await ctx.db.from("card").insert(cardsInsert);
|
||||||
}
|
|
||||||
|
|
||||||
listIndex ++
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
|
|
||||||
boardsCreated ++
|
listIndex++;
|
||||||
|
}
|
||||||
|
boardsCreated++;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (boardsCreated > 0 && newImportId) {
|
if (boardsCreated > 0 && newImportId) {
|
||||||
await ctx.db.update(imports)
|
await ctx.db
|
||||||
.set({ status: 'success' })
|
.from("import")
|
||||||
.where(eq(imports.id, newImportId))
|
.update({ status: "success" })
|
||||||
|
.eq("importId", newImportId);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { boardsCreated }
|
return { boardsCreated };
|
||||||
}),
|
}),
|
||||||
})
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { cards, cardsToLabels, labels } from "~/server/db/schema";
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const labelRouter = createTRPCRouter({
|
export const labelRouter = createTRPCRouter({
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1).max(36),
|
name: z.string().min(1).max(36),
|
||||||
@@ -18,41 +12,41 @@ export const labelRouter = createTRPCRouter({
|
|||||||
colourCode: z.string().length(7),
|
colourCode: z.string().length(7),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const card = await ctx.db
|
||||||
const card = await tx.query.cards.findFirst({
|
.from("card")
|
||||||
where: and(eq(cards.publicId, input.cardPublicId), isNull(cards.deletedAt)),
|
.select(`id, list (boardId)`)
|
||||||
with: {
|
.eq("publicId", input.cardPublicId)
|
||||||
list: {
|
.is("deletedAt", null)
|
||||||
with: {
|
.limit(1)
|
||||||
board: true
|
.single();
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!card) return;
|
if (!card.data?.list) return;
|
||||||
|
|
||||||
const newLabel = await tx.insert(labels).values({
|
const newLabel = await ctx.db
|
||||||
|
.from("label")
|
||||||
|
.insert({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: input.name,
|
name: input.name,
|
||||||
colourCode: input.colourCode,
|
colourCode: input.colourCode,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
boardId: card.list.boardId,
|
boardId: card.data.list.boardId,
|
||||||
}).returning({ id: labels.id });
|
})
|
||||||
|
.select(`id`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const newLabelId = newLabel[0]?.id
|
if (!newLabel.data) return;
|
||||||
|
|
||||||
if (!newLabelId) return;
|
await ctx.db.from("_card_labels").insert({
|
||||||
|
cardId: card.data.id,
|
||||||
|
labelId: newLabel.data.id,
|
||||||
|
});
|
||||||
|
|
||||||
return tx.insert(cardsToLabels).values({
|
return newLabel.data;
|
||||||
cardId: card.id,
|
|
||||||
labelId: newLabelId,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,120 +1,129 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { and, desc, eq, sql, isNull } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { boards, cards, lists } from "~/server/db/schema";
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const listRouter = createTRPCRouter({
|
export const listRouter = createTRPCRouter({
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
boardPublicId: z.string().min(12)
|
boardPublicId: z.string().min(12),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const board = await ctx.db
|
||||||
const board = await tx.query.boards.findFirst({
|
.from("board")
|
||||||
where: eq(boards.publicId, input.boardPublicId),
|
.select(`id, lists:list (index)`)
|
||||||
columns: {
|
.eq("publicId", input.boardPublicId)
|
||||||
id: true
|
.order("index", { foreignTable: "list", ascending: false })
|
||||||
}
|
.is("list.deletedAt", null)
|
||||||
});
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
if (!board) return;
|
if (!board.data) return;
|
||||||
|
|
||||||
const latestList = await tx.query.lists.findFirst({
|
const latestListIndex = board.data.lists[0]?.index;
|
||||||
where: eq(lists.boardId, board.id),
|
|
||||||
columns: {
|
|
||||||
index: true
|
|
||||||
},
|
|
||||||
orderBy: desc(lists.index)
|
|
||||||
});
|
|
||||||
|
|
||||||
return tx.insert(lists).values({
|
const { data } = await ctx.db.from("list").insert({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
name: input.name,
|
name: input.name,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
boardId: board.id,
|
boardId: board.data.id,
|
||||||
index: latestList ? latestList.index + 1 : 0
|
index: latestListIndex ? latestListIndex + 1 : 0,
|
||||||
});
|
}).select(`
|
||||||
})
|
publicId,
|
||||||
|
name
|
||||||
|
`);
|
||||||
|
|
||||||
|
return data;
|
||||||
}),
|
}),
|
||||||
reorder: publicProcedure
|
reorder: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
boardId: z.string().min(12),
|
boardId: z.string().min(12),
|
||||||
listId: z.string().min(12),
|
listId: z.string().min(12),
|
||||||
currentIndex: z.number(),
|
currentIndex: z.number(),
|
||||||
newIndex: z.number(),
|
newIndex: z.number(),
|
||||||
}))
|
}),
|
||||||
.mutation(({ ctx, input }) => {
|
)
|
||||||
const userId = ctx.session?.user.id;
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const list = await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.select(`id, boardId`)
|
||||||
|
.eq("publicId", input.listId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
if (!userId) return;
|
if (!list?.data) return;
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
const { data } = await ctx.db.rpc("reorder_lists", {
|
||||||
const [list] = await tx.select({ id: lists.id, boardId: lists.boardId }).from(lists).where(eq(lists.publicId, input.listId))
|
board_id: list.data.boardId,
|
||||||
|
list_id: list.data.id,
|
||||||
|
current_index: input.currentIndex,
|
||||||
|
new_index: input.newIndex,
|
||||||
|
});
|
||||||
|
|
||||||
if (!list) return;
|
return data;
|
||||||
|
|
||||||
await tx.execute(sql`
|
|
||||||
UPDATE ${lists}
|
|
||||||
SET index =
|
|
||||||
CASE
|
|
||||||
WHEN ${lists.index} = ${input.currentIndex} AND ${lists.id} = ${list.id} THEN ${input.newIndex}
|
|
||||||
WHEN ${input.currentIndex} < ${input.newIndex} AND ${lists.index} > ${input.currentIndex} AND ${lists.index} <= ${input.newIndex} THEN ${lists.index} - 1
|
|
||||||
WHEN ${input.currentIndex} > ${input.newIndex} AND ${lists.index} >= ${input.newIndex} AND ${lists.index} < ${input.currentIndex} THEN ${lists.index} + 1
|
|
||||||
ELSE ${lists.index}
|
|
||||||
END
|
|
||||||
WHERE ${lists.boardId} = ${list.boardId};
|
|
||||||
`);
|
|
||||||
})
|
|
||||||
}),
|
}),
|
||||||
delete: publicProcedure
|
delete: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
listPublicId: z.string().min(12),
|
listPublicId: z.string().min(12),
|
||||||
}))
|
}),
|
||||||
.mutation(({ ctx, input }) => {
|
)
|
||||||
const userId = ctx.session?.user.id;
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
const list = await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.select(`id, boardId, index`)
|
||||||
|
.eq("publicId", input.listPublicId)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
return ctx.db.transaction(async (tx) => {
|
if (!list.data) return;
|
||||||
const list = await tx.query.lists.findFirst({
|
|
||||||
where: eq(lists.publicId, input.listPublicId),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!list) return;
|
const deletedAt = new Date().toISOString();
|
||||||
|
|
||||||
await tx.update(lists).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(lists.id, list.id));
|
await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.eq("id", list.data.id)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
|
||||||
await tx.update(cards).set({ deletedAt: new Date(), deletedBy: userId}).where(eq(cards.listId, list.id));
|
await ctx.db
|
||||||
|
.from("card")
|
||||||
|
.update({ deletedAt, deletedBy: userId })
|
||||||
|
.eq("listId", list.data.id)
|
||||||
|
.is("deletedAt", null);
|
||||||
|
|
||||||
await tx.execute(sql`UPDATE ${lists} SET ${lists.index} = ${lists.index} - 1 WHERE ${lists.boardId} = ${list.boardId} AND ${lists.index} > ${list.index} AND ${lists.deletedAt} IS NULL;`);
|
const { data } = await ctx.db.rpc("shift_list_index", {
|
||||||
})
|
board_id: list.data.boardId,
|
||||||
|
list_index: list.data.index,
|
||||||
|
});
|
||||||
|
|
||||||
|
return data;
|
||||||
}),
|
}),
|
||||||
update: publicProcedure
|
update: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
listPublicId: z.string().min(12),
|
listPublicId: z.string().min(12),
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
}))
|
}),
|
||||||
.mutation(({ ctx, input }) => {
|
)
|
||||||
const userId = ctx.session?.user.id;
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const { data } = await ctx.db
|
||||||
|
.from("list")
|
||||||
|
.update({ name: input.name })
|
||||||
|
.eq("publicId", input.listPublicId)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.select(`publicId, name`);
|
||||||
|
|
||||||
if (!userId) return;
|
return data;
|
||||||
|
|
||||||
return ctx.db.update(lists).set({ name: input.name }).where(and(eq(lists.publicId, input.listPublicId), isNull(lists.deletedAt)));
|
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,104 +1,95 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { and, eq, isNull } from "drizzle-orm";
|
|
||||||
|
|
||||||
import { workspaces, workspaceMembers } from "~/server/db/schema";
|
|
||||||
|
|
||||||
import { generateUID } from "~/utils/generateUID";
|
import { generateUID } from "~/utils/generateUID";
|
||||||
|
|
||||||
import {
|
import { createTRPCRouter, protectedProcedure } from "~/server/api/trpc";
|
||||||
createTRPCRouter,
|
|
||||||
publicProcedure,
|
|
||||||
} from "~/server/api/trpc";
|
|
||||||
|
|
||||||
export const workspaceRouter = createTRPCRouter({
|
export const workspaceRouter = createTRPCRouter({
|
||||||
all: publicProcedure
|
all: protectedProcedure.query(async ({ ctx }) => {
|
||||||
.query(({ ctx }) => {
|
const userId = ctx.user?.id;
|
||||||
const userId = ctx.session?.user.id;
|
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
return ctx.db.query.workspaceMembers.findMany({
|
const { data } = await ctx.db
|
||||||
where: and(eq(workspaceMembers.userId, userId), isNull(workspaceMembers.deletedAt)),
|
.from("workspace_members")
|
||||||
columns: {
|
.select(
|
||||||
role: true,
|
`
|
||||||
},
|
role,
|
||||||
with: {
|
workspace (
|
||||||
workspace: {
|
publicId,
|
||||||
columns: {
|
name
|
||||||
publicId: true,
|
)
|
||||||
name: true
|
`,
|
||||||
}
|
)
|
||||||
}
|
.eq("userId", userId)
|
||||||
}
|
.is("deletedAt", null);
|
||||||
});
|
|
||||||
}),
|
return data;
|
||||||
byId: publicProcedure
|
}),
|
||||||
|
byId: protectedProcedure
|
||||||
.input(z.object({ publicId: z.string().min(12) }))
|
.input(z.object({ publicId: z.string().min(12) }))
|
||||||
.query(({ ctx, input }) => {
|
.query(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const { data } = await ctx.db
|
||||||
|
.from("workspace")
|
||||||
|
.select(
|
||||||
|
`
|
||||||
|
publicId,
|
||||||
|
members: workspace_members (
|
||||||
|
publicId,
|
||||||
|
role,
|
||||||
|
user (
|
||||||
|
id,
|
||||||
|
name,
|
||||||
|
email
|
||||||
|
)
|
||||||
|
)
|
||||||
|
`,
|
||||||
|
)
|
||||||
|
.eq("publicId", input.publicId)
|
||||||
|
.is("deletedAt", null)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
if (!userId) return;
|
return data;
|
||||||
|
|
||||||
return ctx.db.query.workspaces.findFirst({
|
|
||||||
where: and(eq(workspaces.publicId, input.publicId), isNull(workspaces.deletedAt)),
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
members: {
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
with: {
|
|
||||||
user: {
|
|
||||||
columns: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}),
|
}),
|
||||||
create: publicProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
name: z.string().min(1),
|
name: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const userId = ctx.session?.user.id;
|
const userId = ctx.user?.id;
|
||||||
|
|
||||||
if (!userId) return;
|
if (!userId) return;
|
||||||
|
|
||||||
const workspace = await ctx.db.insert(workspaces).values({
|
const workspace = await ctx.db
|
||||||
publicId: generateUID(),
|
.from("workspace")
|
||||||
name: input.name,
|
.insert({
|
||||||
slug: input.name.toLowerCase(),
|
publicId: generateUID(),
|
||||||
createdBy: userId,
|
name: input.name,
|
||||||
}).returning({ id: workspaces.id });
|
slug: input.name.toLowerCase(),
|
||||||
|
createdBy: userId,
|
||||||
|
})
|
||||||
|
.select(`id, publicId, name`)
|
||||||
|
.limit(1)
|
||||||
|
.single();
|
||||||
|
|
||||||
const workspaceId = workspace[0]?.id;
|
const newWorkspaceId = workspace.data?.id;
|
||||||
|
|
||||||
if (!workspaceId) return;
|
if (!newWorkspaceId) return;
|
||||||
|
|
||||||
await ctx.db.insert(workspaceMembers).values({
|
await ctx.db.from("workspace_members").insert({
|
||||||
publicId: generateUID(),
|
publicId: generateUID(),
|
||||||
userId,
|
userId,
|
||||||
workspaceId: workspaceId,
|
workspaceId: newWorkspaceId,
|
||||||
createdBy: userId,
|
createdBy: userId,
|
||||||
role: 'admin'
|
role: "admin",
|
||||||
})
|
|
||||||
|
|
||||||
return ctx.db.query.workspaces.findFirst({
|
|
||||||
where: eq(workspaces.id, workspaceId),
|
|
||||||
columns: {
|
|
||||||
publicId: true,
|
|
||||||
name: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const newWorkspace = { ...workspace.data };
|
||||||
|
|
||||||
|
delete newWorkspace.id;
|
||||||
|
|
||||||
|
return newWorkspace;
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,14 +6,14 @@
|
|||||||
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
* TL;DR - This is where all the tRPC server stuff is created and plugged in. The pieces you will
|
||||||
* need to use are documented accordingly near the end.
|
* need to use are documented accordingly near the end.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { initTRPC, TRPCError } from "@trpc/server";
|
import { initTRPC, TRPCError } from "@trpc/server";
|
||||||
import { type NextRequest } from "next/server";
|
import { type FetchCreateContextFnOptions } from "@trpc/server/adapters/fetch";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
import { ZodError } from "zod";
|
import { ZodError } from "zod";
|
||||||
|
|
||||||
import { auth } from "~/server/auth";
|
import { createTRPCClient } from "~/utils/supabase/api";
|
||||||
import { db } from "~/server/db";
|
import { type Database } from "~/types/database.types";
|
||||||
|
import { type SupabaseClient } from "@supabase/supabase-js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 1. CONTEXT
|
* 1. CONTEXT
|
||||||
@@ -23,8 +23,13 @@ import { db } from "~/server/db";
|
|||||||
* These allow you to access things when processing a request, like the database, the session, etc.
|
* These allow you to access things when processing a request, like the database, the session, etc.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|
||||||
interface CreateContextOptions {
|
interface CreateContextOptions {
|
||||||
headers: Headers;
|
user: User | null;
|
||||||
|
db: SupabaseClient<Database>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,13 +42,11 @@ interface CreateContextOptions {
|
|||||||
*
|
*
|
||||||
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
* @see https://create.t3.gg/en/usage/trpc#-serverapitrpcts
|
||||||
*/
|
*/
|
||||||
export const createInnerTRPCContext = async (opts: CreateContextOptions) => {
|
|
||||||
const session = await auth();
|
|
||||||
|
|
||||||
|
export const createInnerTRPCContext = (opts: CreateContextOptions) => {
|
||||||
return {
|
return {
|
||||||
session,
|
user: opts.user,
|
||||||
headers: opts.headers,
|
db: opts.db,
|
||||||
db,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -53,12 +56,17 @@ export const createInnerTRPCContext = async (opts: CreateContextOptions) => {
|
|||||||
*
|
*
|
||||||
* @see https://trpc.io/docs/context
|
* @see https://trpc.io/docs/context
|
||||||
*/
|
*/
|
||||||
export const createTRPCContext = async (opts: { req: NextRequest }) => {
|
export const createTRPCContext = async ({
|
||||||
// Fetch stuff that depends on the request
|
req,
|
||||||
|
resHeaders,
|
||||||
|
}: FetchCreateContextFnOptions) => {
|
||||||
|
const db = createTRPCClient(req, resHeaders);
|
||||||
|
|
||||||
return await createInnerTRPCContext({
|
const {
|
||||||
headers: opts.req.headers,
|
data: { user },
|
||||||
});
|
} = await db.auth.getUser();
|
||||||
|
|
||||||
|
return createInnerTRPCContext({ db, user });
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -83,6 +91,13 @@ const t = initTRPC.context<typeof createTRPCContext>().create({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a server-side caller.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/server/server-side-calls
|
||||||
|
*/
|
||||||
|
// export const createCallerFactory = t.createCallerFactory;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
* 3. ROUTER & PROCEDURE (THE IMPORTANT BIT)
|
||||||
*
|
*
|
||||||
@@ -107,15 +122,13 @@ export const createTRPCRouter = t.router;
|
|||||||
export const publicProcedure = t.procedure;
|
export const publicProcedure = t.procedure;
|
||||||
|
|
||||||
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
/** Reusable middleware that enforces users are logged in before running the procedure. */
|
||||||
const enforceUserIsAuthed = t.middleware(({ ctx, next }) => {
|
const enforceUserIsAuthed = t.middleware(async ({ ctx, next }) => {
|
||||||
if (!ctx.session || !ctx.session.user) {
|
if (!ctx.user) {
|
||||||
throw new TRPCError({ code: "UNAUTHORIZED" });
|
throw new TRPCError({ code: "UNAUTHORIZED" });
|
||||||
}
|
}
|
||||||
|
|
||||||
return next({
|
return next({
|
||||||
ctx: {
|
ctx,
|
||||||
// infers the `session` as non-nullable
|
|
||||||
session: { ...ctx.session, user: ctx.session.user },
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
import { render } from '@react-email/render';
|
|
||||||
import { MagicLinkEmail } from "~/email/emails/magic-link";
|
|
||||||
|
|
||||||
import { DrizzleAdapter } from "@auth/drizzle-adapter";
|
|
||||||
import NextAuth, { type DefaultSession } from "next-auth";
|
|
||||||
|
|
||||||
import { db } from "~/server/db";
|
|
||||||
import { pgTable} from "drizzle-orm/pg-core";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module augmentation for `next-auth` types. Allows us to add custom properties to the `session`
|
|
||||||
* object and keep type safety.
|
|
||||||
*
|
|
||||||
* @see https://next-auth.js.org/getting-started/typescript#module-augmentation
|
|
||||||
*/
|
|
||||||
declare module "next-auth" {
|
|
||||||
interface Session extends DefaultSession {
|
|
||||||
user: {
|
|
||||||
id: string;
|
|
||||||
// ...other properties
|
|
||||||
// role: UserRole;
|
|
||||||
} & DefaultSession["user"];
|
|
||||||
}
|
|
||||||
|
|
||||||
// interface User {
|
|
||||||
// // ...other properties
|
|
||||||
// // role: UserRole;
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const {
|
|
||||||
handlers: { GET, POST },
|
|
||||||
auth,
|
|
||||||
} = NextAuth({
|
|
||||||
callbacks: {
|
|
||||||
session: ({ session, user }) => ({
|
|
||||||
...session,
|
|
||||||
user: {
|
|
||||||
id: user.id,
|
|
||||||
name: session.user.name,
|
|
||||||
email: session.user.email,
|
|
||||||
image: session.user.image,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
adapter: DrizzleAdapter(db, pgTable),
|
|
||||||
pages: {
|
|
||||||
signIn: "/auth/login",
|
|
||||||
// signOut: "/auth/signout",
|
|
||||||
// error: "/auth/error",
|
|
||||||
verifyRequest: "/auth/verify",
|
|
||||||
},
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
id: 'email',
|
|
||||||
type: 'email',
|
|
||||||
from: process.env.EMAIL_FROM ?? '',
|
|
||||||
server: {},
|
|
||||||
maxAge: 24 * 60 * 60,
|
|
||||||
name: 'Email',
|
|
||||||
options: {},
|
|
||||||
async sendVerificationRequest({ identifier: email, url }) {
|
|
||||||
const magicLinkEmail = MagicLinkEmail({ loginUrl: url });
|
|
||||||
const response = await fetch(process.env.EMAIL_URL ?? '', {
|
|
||||||
method: "POST",
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${process.env.EMAIL_TOKEN}`,
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
from: process.env.EMAIL_FROM,
|
|
||||||
to: [email],
|
|
||||||
subject: 'Login via email',
|
|
||||||
html: render(magicLinkEmail),
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
throw new Error('Error sending magic login')
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
],
|
|
||||||
});
|
|
||||||
1
src/server/db/migrations/0001_stale_mattie_franklin.sql
Normal file
1
src/server/db/migrations/0001_stale_mattie_franklin.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE "board" ALTER COLUMN "deletedAt" DROP DEFAULT;
|
||||||
1179
src/server/db/migrations/meta/0001_snapshot.json
Normal file
1179
src/server/db/migrations/meta/0001_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -8,6 +8,13 @@
|
|||||||
"when": 1711571659259,
|
"when": 1711571659259,
|
||||||
"tag": "0000_legal_quicksilver",
|
"tag": "0000_legal_quicksilver",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 1,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1713021051974,
|
||||||
|
"tag": "0001_stale_mattie_franklin",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,6 @@ import {
|
|||||||
bigint,
|
bigint,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import { type AdapterAccount } from "@auth/core/adapters";
|
|
||||||
|
|
||||||
export const importSourceEnum = pgEnum('source', ['trello']);
|
export const importSourceEnum = pgEnum('source', ['trello']);
|
||||||
export const importStatusEnum = pgEnum('status', ['started', 'success', 'failed']);
|
export const importStatusEnum = pgEnum('status', ['started', 'success', 'failed']);
|
||||||
export const memberRoleEnum = pgEnum('role', ['admin', 'member', 'guest']);
|
export const memberRoleEnum = pgEnum('role', ['admin', 'member', 'guest']);
|
||||||
@@ -27,7 +25,7 @@ export const boards = pgTable(
|
|||||||
createdBy: uuid("createdBy").notNull().references(() => users.id),
|
createdBy: uuid("createdBy").notNull().references(() => users.id),
|
||||||
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
updatedAt: timestamp("updatedAt"),
|
updatedAt: timestamp("updatedAt"),
|
||||||
deletedAt: timestamp("deletedAt").defaultNow(),
|
deletedAt: timestamp("deletedAt"),
|
||||||
deletedBy: uuid("deletedBy").references(() => users.id),
|
deletedBy: uuid("deletedBy").references(() => users.id),
|
||||||
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
|
importId: bigint("importId", { mode: "number" }).references(() => imports.id),
|
||||||
workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id),
|
workspaceId: bigint("workspaceId", { mode: "number" }).notNull().references(() => workspaces.id),
|
||||||
@@ -236,7 +234,6 @@ export const users = pgTable("user", {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const usersRelations = relations(users, ({ many }) => ({
|
export const usersRelations = relations(users, ({ many }) => ({
|
||||||
accounts: many(accounts),
|
|
||||||
boards: many(boards),
|
boards: many(boards),
|
||||||
cards: many(cards),
|
cards: many(cards),
|
||||||
imports: many(imports),
|
imports: many(imports),
|
||||||
@@ -244,61 +241,6 @@ export const usersRelations = relations(users, ({ many }) => ({
|
|||||||
workspaces: many(workspaces),
|
workspaces: many(workspaces),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
export const accounts = pgTable(
|
|
||||||
"account",
|
|
||||||
{
|
|
||||||
userId: uuid("userId")
|
|
||||||
.notNull()
|
|
||||||
.references(() => users.id),
|
|
||||||
type: varchar("type", { length: 255 })
|
|
||||||
.$type<AdapterAccount["type"]>()
|
|
||||||
.notNull(),
|
|
||||||
provider: varchar("provider", { length: 255 }).notNull(),
|
|
||||||
providerAccountId: varchar("providerAccountId", { length: 255 }).notNull(),
|
|
||||||
refresh_token: text("refresh_token"),
|
|
||||||
access_token: text("access_token"),
|
|
||||||
expires_at: integer("expires_at"),
|
|
||||||
token_type: varchar("token_type", { length: 255 }),
|
|
||||||
scope: varchar("scope", { length: 255 }),
|
|
||||||
id_token: text("id_token"),
|
|
||||||
session_state: varchar("session_state", { length: 255 }),
|
|
||||||
},
|
|
||||||
(account) => ({
|
|
||||||
compoundKey: primaryKey(account.provider, account.providerAccountId),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const accountsRelations = relations(accounts, ({ one }) => ({
|
|
||||||
user: one(users, { fields: [accounts.userId], references: [users.id] }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const sessions = pgTable(
|
|
||||||
"session",
|
|
||||||
{
|
|
||||||
sessionToken: varchar("sessionToken", { length: 255 })
|
|
||||||
.notNull()
|
|
||||||
.primaryKey(),
|
|
||||||
userId: uuid("userId").notNull().references(() => users.id),
|
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
|
||||||
user: one(users, { fields: [sessions.userId], references: [users.id] }),
|
|
||||||
}));
|
|
||||||
|
|
||||||
export const verificationTokens = pgTable(
|
|
||||||
"verificationToken",
|
|
||||||
{
|
|
||||||
identifier: varchar("identifier", { length: 255 }).notNull(),
|
|
||||||
token: varchar("token", { length: 255 }).notNull(),
|
|
||||||
expires: timestamp("expires", { mode: "date" }).notNull(),
|
|
||||||
},
|
|
||||||
(vt) => ({
|
|
||||||
compoundKey: primaryKey(vt.identifier, vt.token),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const workspaces = pgTable(
|
export const workspaces = pgTable(
|
||||||
"workspace",
|
"workspace",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
||||||
import { loggerLink, unstable_httpBatchStreamLink } from "@trpc/client";
|
|
||||||
import { createTRPCReact } from "@trpc/react-query";
|
|
||||||
import { useState } from "react";
|
|
||||||
|
|
||||||
import { type AppRouter } from "~/server/api/root";
|
|
||||||
import { getUrl, transformer } from "./shared";
|
|
||||||
|
|
||||||
export const api = createTRPCReact<AppRouter>();
|
|
||||||
|
|
||||||
export function TRPCReactProvider(props: {
|
|
||||||
children: React.ReactNode;
|
|
||||||
headers: Headers;
|
|
||||||
}) {
|
|
||||||
const [queryClient] = useState(() => new QueryClient());
|
|
||||||
|
|
||||||
const [trpcClient] = useState(() =>
|
|
||||||
api.createClient({
|
|
||||||
transformer,
|
|
||||||
links: [
|
|
||||||
loggerLink({
|
|
||||||
enabled: (op) =>
|
|
||||||
process.env.NODE_ENV === "development" ||
|
|
||||||
(op.direction === "down" && op.result instanceof Error),
|
|
||||||
}),
|
|
||||||
unstable_httpBatchStreamLink({
|
|
||||||
url: getUrl(),
|
|
||||||
headers() {
|
|
||||||
const heads = new Map(props.headers);
|
|
||||||
heads.set("x-trpc-source", "react");
|
|
||||||
return Object.fromEntries(heads);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
|
||||||
<api.Provider client={trpcClient} queryClient={queryClient}>
|
|
||||||
{props.children}
|
|
||||||
</api.Provider>
|
|
||||||
</QueryClientProvider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
import {
|
|
||||||
createTRPCProxyClient,
|
|
||||||
loggerLink,
|
|
||||||
unstable_httpBatchStreamLink,
|
|
||||||
} from "@trpc/client";
|
|
||||||
import { headers } from "next/headers";
|
|
||||||
|
|
||||||
import { type AppRouter } from "~/server/api/root";
|
|
||||||
import { getUrl, transformer } from "./shared";
|
|
||||||
|
|
||||||
export const api = createTRPCProxyClient<AppRouter>({
|
|
||||||
transformer,
|
|
||||||
links: [
|
|
||||||
loggerLink({
|
|
||||||
enabled: (op) =>
|
|
||||||
process.env.NODE_ENV === "development" ||
|
|
||||||
(op.direction === "down" && op.result instanceof Error),
|
|
||||||
}),
|
|
||||||
unstable_httpBatchStreamLink({
|
|
||||||
url: getUrl(),
|
|
||||||
headers() {
|
|
||||||
const heads = new Map(headers());
|
|
||||||
heads.set("x-trpc-source", "rsc");
|
|
||||||
return Object.fromEntries(heads);
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
});
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
|
||||||
import superjson from "superjson";
|
|
||||||
|
|
||||||
import { type AppRouter } from "~/server/api/root";
|
|
||||||
|
|
||||||
export const transformer = superjson;
|
|
||||||
|
|
||||||
function getBaseUrl() {
|
|
||||||
if (typeof window !== "undefined") return "";
|
|
||||||
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`;
|
|
||||||
return `http://localhost:${process.env.PORT ?? 3000}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getUrl() {
|
|
||||||
return getBaseUrl() + "/api/trpc";
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inference helper for inputs.
|
|
||||||
*
|
|
||||||
* @example type HelloInput = RouterInputs['example']['hello']
|
|
||||||
*/
|
|
||||||
export type RouterInputs = inferRouterInputs<AppRouter>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Inference helper for outputs.
|
|
||||||
*
|
|
||||||
* @example type HelloOutput = RouterOutputs['example']['hello']
|
|
||||||
*/
|
|
||||||
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
|
||||||
31
src/types/contenteditable.d.ts
vendored
Normal file
31
src/types/contenteditable.d.ts
vendored
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
declare module "react-contenteditable" {
|
||||||
|
import React from "react";
|
||||||
|
|
||||||
|
type ChangeEventHandler<T = Element> = (event: React.ChangeEvent<T>) => void;
|
||||||
|
type KeyUpHandler<T = Element> = (event: React.KeyboardEvent<T>) => void;
|
||||||
|
type KeyDownHandler<T = Element> = (event: React.KeyboardEvent<T>) => void;
|
||||||
|
|
||||||
|
interface ContentEditableProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||||
|
html: string;
|
||||||
|
onChange?: ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement>;
|
||||||
|
onBlur?: () => void;
|
||||||
|
onKeyUp?: KeyUpHandler<HTMLTextAreaElement | HTMLInputElement>;
|
||||||
|
onKeyDown?: KeyDownHandler<HTMLTextAreaElement | HTMLInputElement>;
|
||||||
|
disabled?: boolean;
|
||||||
|
tagName?: string;
|
||||||
|
className?: string;
|
||||||
|
style?: React.CSSProperties;
|
||||||
|
innerRef?:
|
||||||
|
| React.RefObject<HTMLElement>
|
||||||
|
| ((instance: HTMLElement | null) => void);
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ContentEditable extends React.Component<ContentEditableProps> {}
|
||||||
|
|
||||||
|
interface ContentEditableElement extends HTMLElement {
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ContentEditable;
|
||||||
|
}
|
||||||
728
src/types/database.types.ts
Normal file
728
src/types/database.types.ts
Normal file
@@ -0,0 +1,728 @@
|
|||||||
|
export type Json =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| { [key: string]: Json | undefined }
|
||||||
|
| Json[]
|
||||||
|
|
||||||
|
export type Database = {
|
||||||
|
public: {
|
||||||
|
Tables: {
|
||||||
|
_card_labels: {
|
||||||
|
Row: {
|
||||||
|
cardId: number
|
||||||
|
labelId: number
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
cardId: number
|
||||||
|
labelId: number
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
cardId?: number
|
||||||
|
labelId?: number
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "_card_labels_cardId_card_id_fk"
|
||||||
|
columns: ["cardId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "card"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "_card_labels_labelId_label_id_fk"
|
||||||
|
columns: ["labelId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "label"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
_card_workspace_members: {
|
||||||
|
Row: {
|
||||||
|
cardId: number
|
||||||
|
workspaceMemberId: number
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
cardId: number
|
||||||
|
workspaceMemberId: number
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
cardId?: number
|
||||||
|
workspaceMemberId?: number
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "_card_workspace_members_cardId_card_id_fk"
|
||||||
|
columns: ["cardId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "card"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "_card_workspace_members_workspaceMemberId_workspace_members_id_"
|
||||||
|
columns: ["workspaceMemberId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "workspace_members"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
account: {
|
||||||
|
Row: {
|
||||||
|
access_token: string | null
|
||||||
|
expires_at: number | null
|
||||||
|
id_token: string | null
|
||||||
|
provider: string
|
||||||
|
providerAccountId: string
|
||||||
|
refresh_token: string | null
|
||||||
|
scope: string | null
|
||||||
|
session_state: string | null
|
||||||
|
token_type: string | null
|
||||||
|
type: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
access_token?: string | null
|
||||||
|
expires_at?: number | null
|
||||||
|
id_token?: string | null
|
||||||
|
provider: string
|
||||||
|
providerAccountId: string
|
||||||
|
refresh_token?: string | null
|
||||||
|
scope?: string | null
|
||||||
|
session_state?: string | null
|
||||||
|
token_type?: string | null
|
||||||
|
type: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
access_token?: string | null
|
||||||
|
expires_at?: number | null
|
||||||
|
id_token?: string | null
|
||||||
|
provider?: string
|
||||||
|
providerAccountId?: string
|
||||||
|
refresh_token?: string | null
|
||||||
|
scope?: string | null
|
||||||
|
session_state?: string | null
|
||||||
|
token_type?: string | null
|
||||||
|
type?: string
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "account_userId_user_id_fk"
|
||||||
|
columns: ["userId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
board: {
|
||||||
|
Row: {
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt: string | null
|
||||||
|
deletedBy: string | null
|
||||||
|
id: number
|
||||||
|
importId: number | null
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt: string | null
|
||||||
|
workspaceId: number
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
workspaceId: number
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
name?: string
|
||||||
|
publicId?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
workspaceId?: number
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "board_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "board_deletedBy_user_id_fk"
|
||||||
|
columns: ["deletedBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "board_importId_import_id_fk"
|
||||||
|
columns: ["importId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "import"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "board_workspaceId_workspace_id_fk"
|
||||||
|
columns: ["workspaceId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "workspace"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
card: {
|
||||||
|
Row: {
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt: string | null
|
||||||
|
deletedBy: string | null
|
||||||
|
description: string | null
|
||||||
|
id: number
|
||||||
|
importId: number | null
|
||||||
|
index: number
|
||||||
|
listId: number
|
||||||
|
publicId: string
|
||||||
|
title: string
|
||||||
|
updatedAt: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
description?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
index: number
|
||||||
|
listId: number
|
||||||
|
publicId: string
|
||||||
|
title: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
description?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
index?: number
|
||||||
|
listId?: number
|
||||||
|
publicId?: string
|
||||||
|
title?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_deletedBy_user_id_fk"
|
||||||
|
columns: ["deletedBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_importId_import_id_fk"
|
||||||
|
columns: ["importId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "import"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "card_listId_list_id_fk"
|
||||||
|
columns: ["listId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "list"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
import: {
|
||||||
|
Row: {
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
id: number
|
||||||
|
publicId: string
|
||||||
|
source: Database["public"]["Enums"]["source"]
|
||||||
|
status: Database["public"]["Enums"]["status"]
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
id?: number
|
||||||
|
publicId: string
|
||||||
|
source: Database["public"]["Enums"]["source"]
|
||||||
|
status: Database["public"]["Enums"]["status"]
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
id?: number
|
||||||
|
publicId?: string
|
||||||
|
source?: Database["public"]["Enums"]["source"]
|
||||||
|
status?: Database["public"]["Enums"]["status"]
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "import_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
label: {
|
||||||
|
Row: {
|
||||||
|
boardId: number
|
||||||
|
colourCode: string | null
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
id: number
|
||||||
|
importId: number | null
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
boardId: number
|
||||||
|
colourCode?: string | null
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
boardId?: number
|
||||||
|
colourCode?: string | null
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
name?: string
|
||||||
|
publicId?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "label_boardId_board_id_fk"
|
||||||
|
columns: ["boardId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "board"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "label_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "label_importId_import_id_fk"
|
||||||
|
columns: ["importId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "import"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
list: {
|
||||||
|
Row: {
|
||||||
|
boardId: number
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt: string | null
|
||||||
|
deletedBy: string | null
|
||||||
|
id: number
|
||||||
|
importId: number | null
|
||||||
|
index: number
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
boardId: number
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
index: number
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
boardId?: number
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
importId?: number | null
|
||||||
|
index?: number
|
||||||
|
name?: string
|
||||||
|
publicId?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "list_boardId_board_id_fk"
|
||||||
|
columns: ["boardId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "board"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "list_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "list_deletedBy_user_id_fk"
|
||||||
|
columns: ["deletedBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "list_importId_import_id_fk"
|
||||||
|
columns: ["importId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "import"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
session: {
|
||||||
|
Row: {
|
||||||
|
expires: string
|
||||||
|
sessionToken: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
expires: string
|
||||||
|
sessionToken: string
|
||||||
|
userId: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
expires?: string
|
||||||
|
sessionToken?: string
|
||||||
|
userId?: string
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "session_userId_user_id_fk"
|
||||||
|
columns: ["userId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
user: {
|
||||||
|
Row: {
|
||||||
|
email: string
|
||||||
|
emailVerified: string | null
|
||||||
|
id: string
|
||||||
|
image: string | null
|
||||||
|
name: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
email: string
|
||||||
|
emailVerified?: string | null
|
||||||
|
id: string
|
||||||
|
image?: string | null
|
||||||
|
name?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
email?: string
|
||||||
|
emailVerified?: string | null
|
||||||
|
id?: string
|
||||||
|
image?: string | null
|
||||||
|
name?: string | null
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
verificationToken: {
|
||||||
|
Row: {
|
||||||
|
expires: string
|
||||||
|
identifier: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
expires: string
|
||||||
|
identifier: string
|
||||||
|
token: string
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
expires?: string
|
||||||
|
identifier?: string
|
||||||
|
token?: string
|
||||||
|
}
|
||||||
|
Relationships: []
|
||||||
|
}
|
||||||
|
workspace: {
|
||||||
|
Row: {
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt: string | null
|
||||||
|
deletedBy: string | null
|
||||||
|
id: number
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
slug: string
|
||||||
|
updatedAt: string | null
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
name: string
|
||||||
|
publicId: string
|
||||||
|
slug: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
deletedBy?: string | null
|
||||||
|
id?: number
|
||||||
|
name?: string
|
||||||
|
publicId?: string
|
||||||
|
slug?: string
|
||||||
|
updatedAt?: string | null
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "workspace_createdBy_user_id_fk"
|
||||||
|
columns: ["createdBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "workspace_deletedBy_user_id_fk"
|
||||||
|
columns: ["deletedBy"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
workspace_members: {
|
||||||
|
Row: {
|
||||||
|
createdAt: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt: string | null
|
||||||
|
id: number
|
||||||
|
publicId: string
|
||||||
|
role: Database["public"]["Enums"]["role"]
|
||||||
|
updatedAt: string | null
|
||||||
|
userId: string
|
||||||
|
workspaceId: number
|
||||||
|
}
|
||||||
|
Insert: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
id?: number
|
||||||
|
publicId: string
|
||||||
|
role: Database["public"]["Enums"]["role"]
|
||||||
|
updatedAt?: string | null
|
||||||
|
userId: string
|
||||||
|
workspaceId: number
|
||||||
|
}
|
||||||
|
Update: {
|
||||||
|
createdAt?: string
|
||||||
|
createdBy?: string
|
||||||
|
deletedAt?: string | null
|
||||||
|
id?: number
|
||||||
|
publicId?: string
|
||||||
|
role?: Database["public"]["Enums"]["role"]
|
||||||
|
updatedAt?: string | null
|
||||||
|
userId?: string
|
||||||
|
workspaceId?: number
|
||||||
|
}
|
||||||
|
Relationships: [
|
||||||
|
{
|
||||||
|
foreignKeyName: "workspace_members_userId_user_id_fk"
|
||||||
|
columns: ["userId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "user"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
foreignKeyName: "workspace_members_workspaceId_workspace_id_fk"
|
||||||
|
columns: ["workspaceId"]
|
||||||
|
isOneToOne: false
|
||||||
|
referencedRelation: "workspace"
|
||||||
|
referencedColumns: ["id"]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Views: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
Functions: {
|
||||||
|
reorder_cards: {
|
||||||
|
Args: {
|
||||||
|
card_id: number
|
||||||
|
current_list_id: number
|
||||||
|
new_list_id: number
|
||||||
|
current_index: number
|
||||||
|
new_index: number
|
||||||
|
}
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
|
reorder_lists: {
|
||||||
|
Args: {
|
||||||
|
board_id: number
|
||||||
|
list_id: number
|
||||||
|
current_index: number
|
||||||
|
new_index: number
|
||||||
|
}
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
|
shift_card_index: {
|
||||||
|
Args: {
|
||||||
|
list_id: number
|
||||||
|
card_index: number
|
||||||
|
}
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
|
shift_list_index: {
|
||||||
|
Args: {
|
||||||
|
board_id: number
|
||||||
|
list_index: number
|
||||||
|
}
|
||||||
|
Returns: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Enums: {
|
||||||
|
role: "admin" | "member" | "guest"
|
||||||
|
source: "trello"
|
||||||
|
status: "started" | "success" | "failed"
|
||||||
|
}
|
||||||
|
CompositeTypes: {
|
||||||
|
[_ in never]: never
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PublicSchema = Database[Extract<keyof Database, "public">]
|
||||||
|
|
||||||
|
export type Tables<
|
||||||
|
PublicTableNameOrOptions extends
|
||||||
|
| keyof (PublicSchema["Tables"] & PublicSchema["Views"])
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? keyof (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
Database[PublicTableNameOrOptions["schema"]]["Views"])
|
||||||
|
: never = never,
|
||||||
|
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? (Database[PublicTableNameOrOptions["schema"]]["Tables"] &
|
||||||
|
Database[PublicTableNameOrOptions["schema"]]["Views"])[TableName] extends {
|
||||||
|
Row: infer R
|
||||||
|
}
|
||||||
|
? R
|
||||||
|
: never
|
||||||
|
: PublicTableNameOrOptions extends keyof (PublicSchema["Tables"] &
|
||||||
|
PublicSchema["Views"])
|
||||||
|
? (PublicSchema["Tables"] &
|
||||||
|
PublicSchema["Views"])[PublicTableNameOrOptions] extends {
|
||||||
|
Row: infer R
|
||||||
|
}
|
||||||
|
? R
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type TablesInsert<
|
||||||
|
PublicTableNameOrOptions extends
|
||||||
|
| keyof PublicSchema["Tables"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||||
|
: never = never,
|
||||||
|
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
|
Insert: infer I
|
||||||
|
}
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||||
|
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||||
|
Insert: infer I
|
||||||
|
}
|
||||||
|
? I
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type TablesUpdate<
|
||||||
|
PublicTableNameOrOptions extends
|
||||||
|
| keyof PublicSchema["Tables"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
TableName extends PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? keyof Database[PublicTableNameOrOptions["schema"]]["Tables"]
|
||||||
|
: never = never,
|
||||||
|
> = PublicTableNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[PublicTableNameOrOptions["schema"]]["Tables"][TableName] extends {
|
||||||
|
Update: infer U
|
||||||
|
}
|
||||||
|
? U
|
||||||
|
: never
|
||||||
|
: PublicTableNameOrOptions extends keyof PublicSchema["Tables"]
|
||||||
|
? PublicSchema["Tables"][PublicTableNameOrOptions] extends {
|
||||||
|
Update: infer U
|
||||||
|
}
|
||||||
|
? U
|
||||||
|
: never
|
||||||
|
: never
|
||||||
|
|
||||||
|
export type Enums<
|
||||||
|
PublicEnumNameOrOptions extends
|
||||||
|
| keyof PublicSchema["Enums"]
|
||||||
|
| { schema: keyof Database },
|
||||||
|
EnumName extends PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||||
|
? keyof Database[PublicEnumNameOrOptions["schema"]]["Enums"]
|
||||||
|
: never = never,
|
||||||
|
> = PublicEnumNameOrOptions extends { schema: keyof Database }
|
||||||
|
? Database[PublicEnumNameOrOptions["schema"]]["Enums"][EnumName]
|
||||||
|
: PublicEnumNameOrOptions extends keyof PublicSchema["Enums"]
|
||||||
|
? PublicSchema["Enums"][PublicEnumNameOrOptions]
|
||||||
|
: never
|
||||||
92
src/utils/api.ts
Normal file
92
src/utils/api.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/**
|
||||||
|
* This is the client-side entrypoint for your tRPC API. It is used to create the `api` object which
|
||||||
|
* contains the Next.js App-wrapper, as well as your type-safe React Query hooks.
|
||||||
|
*
|
||||||
|
* We also create a few inference helpers for input and output types.
|
||||||
|
*/
|
||||||
|
import { httpBatchLink, loggerLink, type TRPCLink } from "@trpc/client";
|
||||||
|
import { observable } from "@trpc/server/observable";
|
||||||
|
import { createTRPCNext } from "@trpc/next";
|
||||||
|
import { type inferRouterInputs, type inferRouterOutputs } from "@trpc/server";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
import { type AppRouter } from "~/server/api/root";
|
||||||
|
|
||||||
|
const authLink: TRPCLink<AppRouter> = () => {
|
||||||
|
return ({ next, op }) => {
|
||||||
|
return observable((observer) => {
|
||||||
|
const unsubscribe = next(op).subscribe({
|
||||||
|
next(value) {
|
||||||
|
observer.next(value);
|
||||||
|
},
|
||||||
|
error(err) {
|
||||||
|
if (typeof window !== "undefined" && err.message === "UNAUTHORIZED") {
|
||||||
|
window.location.href = "/login";
|
||||||
|
}
|
||||||
|
observer.error(err);
|
||||||
|
},
|
||||||
|
complete() {
|
||||||
|
observer.complete();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return unsubscribe;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getBaseUrl = () => {
|
||||||
|
if (typeof window !== "undefined") return ""; // browser should use relative url
|
||||||
|
if (process.env.VERCEL_URL) return `https://${process.env.VERCEL_URL}`; // SSR should use vercel url
|
||||||
|
return `http://localhost:${process.env.PORT ?? 3000}`; // dev SSR should use localhost
|
||||||
|
};
|
||||||
|
|
||||||
|
/** A set of type-safe react-query hooks for your tRPC API. */
|
||||||
|
export const api = createTRPCNext<AppRouter>({
|
||||||
|
config() {
|
||||||
|
return {
|
||||||
|
/**
|
||||||
|
* Links used to determine request flow from client to server.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/links
|
||||||
|
*/
|
||||||
|
links: [
|
||||||
|
loggerLink({
|
||||||
|
enabled: (opts) =>
|
||||||
|
process.env.NODE_ENV === "development" ||
|
||||||
|
(opts.direction === "down" && opts.result instanceof Error),
|
||||||
|
}),
|
||||||
|
authLink,
|
||||||
|
httpBatchLink({
|
||||||
|
/**
|
||||||
|
* Transformer used for data de-serialization from the server.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/data-transformers
|
||||||
|
*/
|
||||||
|
transformer: superjson,
|
||||||
|
url: `${getBaseUrl()}/api/trpc`,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Whether tRPC should await queries when server rendering pages.
|
||||||
|
*
|
||||||
|
* @see https://trpc.io/docs/nextjs#ssr-boolean-default-false
|
||||||
|
*/
|
||||||
|
ssr: false,
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inference helper for inputs.
|
||||||
|
*
|
||||||
|
* @example type HelloInput = RouterInputs['example']['hello']
|
||||||
|
*/
|
||||||
|
export type RouterInputs = inferRouterInputs<AppRouter>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inference helper for outputs.
|
||||||
|
*
|
||||||
|
* @example type HelloOutput = RouterOutputs['example']['hello']
|
||||||
|
*/
|
||||||
|
export type RouterOutputs = inferRouterOutputs<AppRouter>;
|
||||||
58
src/utils/supabase/api.ts
Normal file
58
src/utils/supabase/api.ts
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
import {
|
||||||
|
createServerClient,
|
||||||
|
type CookieOptions,
|
||||||
|
serialize,
|
||||||
|
} from "@supabase/ssr";
|
||||||
|
import { RequestCookies } from "@edge-runtime/cookies";
|
||||||
|
import { type Database } from "~/types/database.types";
|
||||||
|
|
||||||
|
import { type NextApiRequest, type NextApiResponse } from "next";
|
||||||
|
|
||||||
|
export function createNextClient(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const supabase = createServerClient<Database>(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
get(name: string) {
|
||||||
|
return req.cookies[name];
|
||||||
|
},
|
||||||
|
set(name: string, value: string, options: CookieOptions) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||||
|
res.appendHeader("Set-Cookie", serialize(name, value, options));
|
||||||
|
},
|
||||||
|
remove(name: string, options: CookieOptions) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||||
|
res.appendHeader("Set-Cookie", serialize(name, "", options));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return supabase;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTRPCClient(req: Request, resHeaders: Headers) {
|
||||||
|
const supabase = createServerClient<Database>(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
{
|
||||||
|
cookies: {
|
||||||
|
get(name: string) {
|
||||||
|
const cookies = new RequestCookies(req.headers);
|
||||||
|
return cookies.get(name)?.value;
|
||||||
|
},
|
||||||
|
set(name: string, value: string, options: CookieOptions) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||||
|
resHeaders.set("Set-Cookie", serialize(name, value, options));
|
||||||
|
},
|
||||||
|
remove(name: string, options: CookieOptions) {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-call
|
||||||
|
resHeaders.set("Set-Cookie", serialize(name, "", options));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
return supabase;
|
||||||
|
}
|
||||||
11
src/utils/supabase/client.ts
Normal file
11
src/utils/supabase/client.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { createBrowserClient } from "@supabase/ssr";
|
||||||
|
import { type Database } from "~/types/database.types";
|
||||||
|
|
||||||
|
export default function createClient() {
|
||||||
|
const supabase = createBrowserClient<Database>(
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_URL!,
|
||||||
|
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
|
||||||
|
);
|
||||||
|
|
||||||
|
return supabase;
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Login } from "~/app/components/auth";
|
import { Login } from "~/components/auth";
|
||||||
|
|
||||||
export default function LoginPage() {
|
export default function LoginPage() {
|
||||||
return (
|
return (
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
export default function BoardDropdown() {
|
export default function BoardDropdown() {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
@@ -11,7 +9,7 @@ export default function BoardDropdown() {
|
|||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="hover:bg-light-200 flex h-8 w-8 items-center justify-center rounded-[5px] dark:hover:bg-dark-200">
|
<Menu.Button className="flex h-8 w-8 items-center justify-center rounded-[5px] hover:bg-light-200 dark:hover:bg-dark-200">
|
||||||
<HiEllipsisHorizontal
|
<HiEllipsisHorizontal
|
||||||
size={25}
|
size={25}
|
||||||
className="text-light-900 dark:text-dark-900"
|
className="text-light-900 dark:text-dark-900"
|
||||||
@@ -28,12 +26,12 @@ export default function BoardDropdown() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-200 absolute right-0 z-30 mt-2 w-56 origin-top-right rounded-md border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
|
<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">
|
<div className="flex">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal("DELETE_BOARD")}
|
onClick={() => openModal("DELETE_BOARD")}
|
||||||
className="hover:bg-light-200 m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 dark:text-dark-1000 dark:hover:bg-dark-400"
|
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
|
Delete board
|
||||||
</button>
|
</button>
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { useBoard } from "~/app/providers/board";
|
import { useBoard } from "~/providers/board";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
export function DeleteBoardConfirmation() {
|
export function DeleteBoardConfirmation() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -24,13 +22,13 @@ export function DeleteBoardConfirmation() {
|
|||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
Are you sure you want to delete this board?
|
Are you sure you want to delete this board?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-light-900 text-sm font-medium dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"This action can't be undone."}
|
{"This action can't be undone."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end sm:mt-6">
|
<div className="mt-5 flex justify-end sm:mt-6">
|
||||||
<button
|
<button
|
||||||
className="bg-light-50 border-light-600 mr-4 inline-flex justify-center rounded-md border-[1px] 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"
|
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()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -41,7 +39,7 @@ export function DeleteBoardConfirmation() {
|
|||||||
boardPublicId: boardData.publicId,
|
boardPublicId: boardData.publicId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-light-1000 text-light-50 inline-flex justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
import { api } from "~/utils/api";
|
||||||
|
import { useBoard } from "~/providers/board";
|
||||||
import { api } from "~/trpc/react";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useBoard } from "~/app/providers/board";
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
|
||||||
|
|
||||||
interface DeleteListConfirmationProps {
|
interface DeleteListConfirmationProps {
|
||||||
listPublicId: string;
|
listPublicId: string;
|
||||||
@@ -31,13 +29,13 @@ export function DeleteListConfirmation({
|
|||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
Are you sure you want to delete this list?
|
Are you sure you want to delete this list?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-light-900 text-sm font-medium dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"This action can't be undone."}
|
{"This action can't be undone."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end sm:mt-6">
|
<div className="mt-5 flex justify-end sm:mt-6">
|
||||||
<button
|
<button
|
||||||
className="bg-light-50 border-light-600 mr-4 inline-flex justify-center rounded-md border-[1px] 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"
|
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()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -48,7 +46,7 @@ export function DeleteListConfirmation({
|
|||||||
listPublicId,
|
listPublicId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-light-1000 text-light-50 inline-flex justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { type ReactNode } from "react";
|
import { type ReactNode } from "react";
|
||||||
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
import ListDropdown from "./ListDropdown";
|
import ListDropdown from "./ListDropdown";
|
||||||
|
|
||||||
@@ -66,7 +64,7 @@ export default function List({
|
|||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
className="bg-light-300 border-light-400 dark-text-dark-1000 mr-5 h-fit min-w-[18rem] max-w-[18rem] rounded-md border py-2 pl-2 pr-1 text-neutral-900 dark:border-dark-400 dark:bg-dark-200"
|
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">
|
<div className="flex justify-between">
|
||||||
<form
|
<form
|
||||||
@@ -85,7 +83,7 @@ export default function List({
|
|||||||
</form>
|
</form>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-400 mx-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 dark:hover:bg-dark-400"
|
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)}
|
onClick={() => openNewCardForm(list.publicId)}
|
||||||
>
|
>
|
||||||
<HiOutlinePlusSmall
|
<HiOutlinePlusSmall
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
interface ListDropdownProps {
|
interface ListDropdownProps {
|
||||||
setSelectedPublicListId: () => void;
|
setSelectedPublicListId: () => void;
|
||||||
@@ -22,7 +20,7 @@ export default function ListDropdown({
|
|||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="hover:bg-light-400 mr-1 inline-flex h-fit items-center rounded-md p-1 px-1 text-sm font-semibold text-dark-50 dark:hover:bg-dark-400">
|
<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" />
|
<HiEllipsisHorizontal className="h-5 w-5 text-dark-900" />
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,12 +34,12 @@ export default function ListDropdown({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 dark-text-dark-1000 border-light-400 absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md border text-neutral-900 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
|
<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">
|
<div className="flex">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={handleOpenDeleteListConfirmation}
|
onClick={handleOpenDeleteListConfirmation}
|
||||||
className="hover:bg-light-400 m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm dark:hover:bg-dark-400"
|
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
|
Delete list
|
||||||
</button>
|
</button>
|
||||||
@@ -1,15 +1,13 @@
|
|||||||
"use client";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
import CheckboxDropdown from "~/app/components/CheckboxDropdown";
|
import CheckboxDropdown from "~/components/CheckboxDropdown";
|
||||||
import { useBoard } from "~/app/providers/board";
|
import { useBoard } from "~/providers/board";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
interface FormData {
|
interface FormData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -90,7 +88,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
const formattedMembers =
|
const formattedMembers =
|
||||||
boardData?.workspace?.members?.map((member) => ({
|
boardData?.workspace?.members?.map((member) => ({
|
||||||
key: member.publicId,
|
key: member.publicId,
|
||||||
value: member.user.name,
|
value: member.user?.name ?? "",
|
||||||
selected: memberPublicIds.includes(member.publicId),
|
selected: memberPublicIds.includes(member.publicId),
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
@@ -142,7 +140,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
New card
|
New card
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-200 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||||
@@ -161,7 +159,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
id="title"
|
id="title"
|
||||||
type="text"
|
type="text"
|
||||||
{...register("title")}
|
{...register("title")}
|
||||||
className="ring-light-600 focus:ring-light-600 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 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"
|
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>
|
||||||
<div className="mt-2 flex space-x-1">
|
<div className="mt-2 flex space-x-1">
|
||||||
@@ -172,7 +170,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
handleSelectList(list.key)
|
handleSelectList(list.key)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="border-light-600 bg-light-200 text-light-800 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] px-2 py-1 text-left text-xs 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}
|
||||||
</div>
|
</div>
|
||||||
</CheckboxDropdown>
|
</CheckboxDropdown>
|
||||||
@@ -184,7 +182,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
handleSelectMembers(list.key)
|
handleSelectMembers(list.key)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="border-light-600 bg-light-200 text-light-800 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] px-2 py-1 text-left text-xs 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 ? (
|
||||||
"Members"
|
"Members"
|
||||||
) : (
|
) : (
|
||||||
@@ -197,7 +195,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
return (
|
return (
|
||||||
<span
|
<span
|
||||||
key={member?.key}
|
key={member?.key}
|
||||||
className="ring-light-200 inline-flex h-4 w-4 items-center justify-center rounded-full bg-gray-400 ring-1 dark:ring-dark-500"
|
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">
|
<span className="text-[8px] font-medium leading-none text-white">
|
||||||
{member?.value
|
{member?.value
|
||||||
@@ -222,7 +220,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
handleSelectLabels(list.key)
|
handleSelectLabels(list.key)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="border-light-600 bg-light-200 text-light-800 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] px-2 py-1 text-left text-xs 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 ? (
|
||||||
"Labels"
|
"Labels"
|
||||||
) : (
|
) : (
|
||||||
@@ -267,7 +265,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-3 flex items-center justify-end">
|
<div className="mt-3 flex items-center justify-end">
|
||||||
<span className="text-light-900 mr-2 text-xs dark:text-dark-900">
|
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||||
Create more
|
Create more
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -294,7 +292,7 @@ export function NewCardForm({ listPublicId }: NewCardFormProps) {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Create card
|
||||||
</button>
|
</button>
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
import { Switch } from "@headlessui/react";
|
import { Switch } from "@headlessui/react";
|
||||||
|
|
||||||
import { useBoard } from "~/app/providers/board";
|
import { useBoard } from "~/providers/board";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
|
|
||||||
@@ -56,7 +54,7 @@ export function NewListForm({ boardPublicId }: boardPublicId) {
|
|||||||
New list
|
New list
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-200 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
className="rounded p-1 hover:bg-light-200 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||||
@@ -85,10 +83,10 @@ export function NewListForm({ boardPublicId }: boardPublicId) {
|
|||||||
<Field
|
<Field
|
||||||
id="list-name"
|
id="list-name"
|
||||||
name="name"
|
name="name"
|
||||||
className="ring-light-600 focus:ring-light-600 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 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"
|
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">
|
<div className="mt-3 flex items-center justify-end">
|
||||||
<span className="text-light-900 mr-2 text-xs dark:text-dark-900">
|
<span className="mr-2 text-xs text-light-900 dark:text-dark-900">
|
||||||
Create more
|
Create more
|
||||||
</span>
|
</span>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -114,7 +112,7 @@ export function NewListForm({ boardPublicId }: boardPublicId) {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Create list
|
||||||
</button>
|
</button>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
@@ -12,17 +10,17 @@ import {
|
|||||||
} from "react-beautiful-dnd";
|
} from "react-beautiful-dnd";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { useBoard } from "~/app/providers/board";
|
import { useBoard } from "~/providers/board";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
import Modal from "~/app/components/modal";
|
import Modal from "~/components/modal";
|
||||||
|
|
||||||
import BoardDropdown from "./components/BoardDropdown";
|
import BoardDropdown from "./components/BoardDropdown";
|
||||||
import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation";
|
import { DeleteBoardConfirmation } from "./components/DeleteBoardConfirmation";
|
||||||
import { DeleteListConfirmation } from "./components/DeleteListConfirmation";
|
import { DeleteListConfirmation } from "./components/DeleteListConfirmation";
|
||||||
import List from "./components/List";
|
import List from "./components/List";
|
||||||
import { NewWorkspaceForm } from "~/app/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";
|
||||||
|
|
||||||
@@ -40,20 +38,12 @@ interface Card {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface Label {
|
interface Label {
|
||||||
label: LabelData;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface LabelData {
|
|
||||||
publicId: string;
|
publicId: string;
|
||||||
name: string;
|
name: string;
|
||||||
colourCode: string;
|
colourCode: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Member {
|
interface Member {
|
||||||
member: MemberData;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MemberData {
|
|
||||||
publicId: string;
|
publicId: string;
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
@@ -76,6 +66,8 @@ export default function BoardPage() {
|
|||||||
const [selectedPublicListId, setSelectedPublicListId] =
|
const [selectedPublicListId, setSelectedPublicListId] =
|
||||||
useState<PublicListId>("");
|
useState<PublicListId>("");
|
||||||
|
|
||||||
|
console.log({ boardData });
|
||||||
|
|
||||||
const boardId = params?.boardId?.length ? params.boardId[0] : null;
|
const boardId = params?.boardId?.length ? params.boardId[0] : null;
|
||||||
|
|
||||||
const updateBoard = api.board.update.useMutation();
|
const updateBoard = api.board.update.useMutation();
|
||||||
@@ -94,17 +86,20 @@ export default function BoardPage() {
|
|||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!boardId) return <></>;
|
const { data, isSuccess } = api.board.byId.useQuery(
|
||||||
|
{ id: boardId ?? "" },
|
||||||
api.board.byId.useQuery(
|
|
||||||
{ id: boardId },
|
|
||||||
{
|
{
|
||||||
onSuccess: (data) => {
|
enabled: !!boardId,
|
||||||
if (data) setBoardData(data);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (isSuccess && data) {
|
||||||
|
console.log({ data });
|
||||||
|
setBoardData(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!boardId) return <></>;
|
||||||
|
|
||||||
const openNewListForm = (publicBoardId: string) => {
|
const openNewListForm = (publicBoardId: string) => {
|
||||||
openModal("NEW_LIST");
|
openModal("NEW_LIST");
|
||||||
setSelectedPublicListId(publicBoardId);
|
setSelectedPublicListId(publicBoardId);
|
||||||
@@ -277,7 +272,7 @@ export default function BoardPage() {
|
|||||||
{(card.labels?.length ?? 0) ||
|
{(card.labels?.length ?? 0) ||
|
||||||
(card.members?.length ?? 0) ? (
|
(card.members?.length ?? 0) ? (
|
||||||
<div className="mt-2 flex justify-end space-x-1">
|
<div className="mt-2 flex justify-end space-x-1">
|
||||||
{card.labels?.map(({ label }) => (
|
{card.labels?.map((label) => (
|
||||||
<span
|
<span
|
||||||
key={label.publicId}
|
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"
|
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"
|
||||||
@@ -294,7 +289,7 @@ export default function BoardPage() {
|
|||||||
</span>
|
</span>
|
||||||
))}
|
))}
|
||||||
<div className="isolate flex -space-x-1 overflow-hidden">
|
<div className="isolate flex -space-x-1 overflow-hidden">
|
||||||
{card.members?.map(({ member }) => (
|
{card.members?.map((member) => (
|
||||||
<span
|
<span
|
||||||
key={member.publicId}
|
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"
|
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"
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
export function BoardsList() {
|
export function BoardsList() {
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
import { FaTrello } from "react-icons/fa";
|
import { FaTrello } from "react-icons/fa";
|
||||||
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
@@ -41,7 +39,7 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Listbox.Button className="ring-light-600 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 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">
|
<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">
|
<span className="flex items-center">
|
||||||
<FaTrello />
|
<FaTrello />
|
||||||
<span className="ml-2 block truncate">
|
<span className="ml-2 block truncate">
|
||||||
@@ -63,14 +61,14 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="bg-light-50 ring-light-600 absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md py-1 text-base text-neutral-900 shadow-lg ring-1 ring-opacity-5 focus:outline-none dark:bg-dark-300 dark:text-dark-1000 sm:text-sm">
|
<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) => (
|
{sources.map(({ source }, index) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={`source_${index}`}
|
key={`source_${index}`}
|
||||||
className="relative cursor-default select-none px-1"
|
className="relative cursor-default select-none px-1"
|
||||||
value={source}
|
value={source}
|
||||||
>
|
>
|
||||||
<div className="hover:bg-light-200 flex items-center rounded-[5px] p-1 dark:hover:bg-dark-400">
|
<div className="flex items-center rounded-[5px] p-1 hover:bg-light-200 dark:hover:bg-dark-400">
|
||||||
<FaTrello className="ml-1" />
|
<FaTrello className="ml-1" />
|
||||||
<span className="ml-2 block truncate font-normal">
|
<span className="ml-2 block truncate font-normal">
|
||||||
{source}
|
{source}
|
||||||
@@ -87,7 +85,7 @@ const SelectSource = ({ handleNextStep }: { handleNextStep: () => void }) => {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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"
|
||||||
>
|
>
|
||||||
Select source
|
Select source
|
||||||
</button>
|
</button>
|
||||||
@@ -219,7 +217,7 @@ const ImportTrello: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
id="apiKey"
|
id="apiKey"
|
||||||
name="apiKey"
|
name="apiKey"
|
||||||
className="ring-light-600 focus:ring-light-700 mb-2 block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
className="mb-2 block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-700 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
||||||
value={formik.values.apiKey}
|
value={formik.values.apiKey}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
@@ -232,14 +230,14 @@ const ImportTrello: React.FC = () => {
|
|||||||
<input
|
<input
|
||||||
id="token"
|
id="token"
|
||||||
name="token"
|
name="token"
|
||||||
className="ring-light-600 focus:ring-light-700 mb-2 block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
className="mb-2 block w-full rounded-md border-0 bg-dark-300 bg-white/5 py-1.5 shadow-sm ring-1 ring-inset ring-light-600 focus:ring-2 focus:ring-inset focus:ring-light-700 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
||||||
value={formik.values.token}
|
value={formik.values.token}
|
||||||
onChange={formik.handleChange}
|
onChange={formik.handleChange}
|
||||||
/>
|
/>
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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"
|
||||||
>
|
>
|
||||||
Fetch boards
|
Fetch boards
|
||||||
</button>
|
</button>
|
||||||
@@ -259,7 +257,7 @@ export function ImportBoardsForm() {
|
|||||||
New import
|
New import
|
||||||
</h2>
|
</h2>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-200 rounded p-1 dark:hover:bg-dark-300"
|
className="rounded p-1 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-dark-900" />
|
<HiXMark size={18} className="text-dark-900" />
|
||||||
@@ -1,11 +1,9 @@
|
|||||||
"use client";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
|
||||||
|
|
||||||
import { HiXMark } from "react-icons/hi2";
|
import { HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
|
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
|
|
||||||
@@ -32,7 +30,7 @@ export function NewBoardForm() {
|
|||||||
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
|
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
|
||||||
<h2 className="text-sm font-bold">New board</h2>
|
<h2 className="text-sm font-bold">New board</h2>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||||
@@ -60,12 +58,12 @@ export function NewBoardForm() {
|
|||||||
<Field
|
<Field
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
className="ring-light-600 focus:ring-light-600 block w-full rounded-md border-0 bg-white/5 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset dark:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
className="block w-full rounded-md border-0 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:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
||||||
/>
|
/>
|
||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Create board
|
||||||
</button>
|
</button>
|
||||||
@@ -1,14 +1,12 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiArrowDownTray, HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
import { BoardsList } from "./components/BoardsList";
|
import { BoardsList } from "./components/BoardsList";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import Modal from "~/app/components/modal";
|
import Modal from "~/components/modal";
|
||||||
|
|
||||||
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
import { ImportBoardsForm } from "./components/ImportBoardsForm";
|
||||||
import { NewBoardForm } from "./components/NewBoardForm";
|
import { NewBoardForm } from "./components/NewBoardForm";
|
||||||
import { NewWorkspaceForm } from "~/app/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
|
|
||||||
export default function BoardsPage() {
|
export default function BoardsPage() {
|
||||||
const { openModal, modalContentType } = useModal();
|
const { openModal, modalContentType } = useModal();
|
||||||
@@ -22,7 +20,7 @@ export default function BoardsPage() {
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-dark-3000 border-light-600 mr-2 flex items-center gap-x-1.5 rounded-md border-[1px] 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"
|
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")}
|
onClick={() => openModal("IMPORT_BOARDS")}
|
||||||
>
|
>
|
||||||
<div className="flex h-5 w-5 items-center">
|
<div className="flex h-5 w-5 items-center">
|
||||||
@@ -32,7 +30,7 @@ export default function BoardsPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-light-1000 text-light-50 flex items-center gap-x-1.5 rounded-md px-3 py-2 text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-dark-1000 dark:text-dark-50"
|
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")}
|
onClick={() => openModal("NEW_BOARD")}
|
||||||
>
|
>
|
||||||
<div className="h-5 w-5 items-center">
|
<div className="h-5 w-5 items-center">
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
interface DeleteCardConfirmationProps {
|
interface DeleteCardConfirmationProps {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
@@ -29,13 +27,13 @@ export function DeleteCardConfirmation({
|
|||||||
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
<h2 className="text-md pb-4 font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
Are you sure you want to delete this card?
|
Are you sure you want to delete this card?
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-light-900 text-sm font-medium dark:text-dark-900">
|
<p className="text-sm font-medium text-light-900 dark:text-dark-900">
|
||||||
{"This action can't be undone."}
|
{"This action can't be undone."}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-5 flex justify-end sm:mt-6">
|
<div className="mt-5 flex justify-end sm:mt-6">
|
||||||
<button
|
<button
|
||||||
className="bg-light-50 border-light-600 mr-4 inline-flex justify-center rounded-md border-[1px] 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"
|
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()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
Cancel
|
Cancel
|
||||||
@@ -46,7 +44,7 @@ export function DeleteCardConfirmation({
|
|||||||
cardPublicId,
|
cardPublicId,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
className="bg-light-1000 text-light-50 inline-flex justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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
|
Delete
|
||||||
</button>
|
</button>
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
import { HiEllipsisHorizontal } from "react-icons/hi2";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
export default function Dropdown() {
|
export default function Dropdown() {
|
||||||
const { openModal } = useModal();
|
const { openModal } = useModal();
|
||||||
@@ -11,7 +9,7 @@ export default function Dropdown() {
|
|||||||
return (
|
return (
|
||||||
<Menu as="div" className="relative inline-block text-left">
|
<Menu as="div" className="relative inline-block text-left">
|
||||||
<div>
|
<div>
|
||||||
<Menu.Button className="hover:bg-light-200 flex h-8 w-8 items-center justify-center rounded-[5px] dark:hover:bg-dark-200">
|
<Menu.Button className="flex h-8 w-8 items-center justify-center rounded-[5px] hover:bg-light-200 dark:hover:bg-dark-200">
|
||||||
<HiEllipsisHorizontal
|
<HiEllipsisHorizontal
|
||||||
size={25}
|
size={25}
|
||||||
className="text-light-900 dark:text-dark-900"
|
className="text-light-900 dark:text-dark-900"
|
||||||
@@ -28,12 +26,12 @@ export default function Dropdown() {
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-200 absolute right-0 z-30 mt-2 w-56 origin-top-right rounded-md border shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-400 dark:bg-dark-300">
|
<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">
|
<div className="flex">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal("DELETE_CARD")}
|
onClick={() => openModal("DELETE_CARD")}
|
||||||
className="hover:bg-light-200 m-1 w-full rounded-[5px] px-3 py-2 text-left text-sm text-neutral-900 dark:text-dark-1000 dark:hover:bg-dark-400"
|
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 card
|
Delete card
|
||||||
</button>
|
</button>
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { HiMiniPlus } from "react-icons/hi2";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
interface LabelSelectorProps {
|
interface LabelSelectorProps {
|
||||||
cardPublicId: string;
|
cardPublicId: string;
|
||||||
@@ -58,7 +56,7 @@ export default function LabelSelector({
|
|||||||
{selectedLabels.map((label) => (
|
{selectedLabels.map((label) => (
|
||||||
<Menu.Button
|
<Menu.Button
|
||||||
key={label.publicId}
|
key={label.publicId}
|
||||||
className="text-light-800 ring-light-600 my-1 mr-2 inline-flex w-fit items-center gap-x-1.5 rounded-full px-2 py-1 text-[12px] font-medium ring-1 ring-inset dark:text-dark-1000 dark:ring-dark-800"
|
className="my-1 mr-2 inline-flex w-fit items-center gap-x-1.5 rounded-full px-2 py-1 text-[12px] font-medium text-light-800 ring-1 ring-inset ring-light-600 dark:text-dark-1000 dark:ring-dark-800"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
fill={label.colourCode}
|
fill={label.colourCode}
|
||||||
@@ -71,13 +69,13 @@ export default function LabelSelector({
|
|||||||
<div>{label.name}</div>
|
<div>{label.name}</div>
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
))}
|
))}
|
||||||
<Menu.Button className="hover:bg-light-400 my-1 inline-flex w-fit items-center gap-x-1.5 rounded-full py-1 pl-2 pr-4 text-[12px] font-medium text-dark-800 ring-inset ring-dark-800 dark:hover:bg-dark-200">
|
<Menu.Button className="my-1 inline-flex w-fit items-center gap-x-1.5 rounded-full py-1 pl-2 pr-4 text-[12px] font-medium text-dark-800 ring-inset ring-dark-800 hover:bg-light-400 dark:hover:bg-dark-200">
|
||||||
<HiMiniPlus size={16} />
|
<HiMiniPlus size={16} />
|
||||||
Add label
|
Add label
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Menu.Button className="border-light-200 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] pl-2 text-left text-sm text-neutral-900 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
<HiMiniPlus size={22} className="pr-2" />
|
<HiMiniPlus size={22} className="pr-2" />
|
||||||
Add label
|
Add label
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
@@ -92,7 +90,7 @@ export default function LabelSelector({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-600 absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
<Menu.Items className="absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
{labels?.map((label) => (
|
{labels?.map((label) => (
|
||||||
@@ -100,7 +98,7 @@ export default function LabelSelector({
|
|||||||
{() => (
|
{() => (
|
||||||
<div
|
<div
|
||||||
key={label.publicId}
|
key={label.publicId}
|
||||||
className="hover:bg-light-200 flex items-center rounded-[5px] p-2 dark:hover:bg-dark-300"
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await formik.setFieldValue(
|
await formik.setFieldValue(
|
||||||
@@ -137,7 +135,7 @@ export default function LabelSelector({
|
|||||||
))}
|
))}
|
||||||
<button
|
<button
|
||||||
onClick={() => openModal("NEW_LABEL")}
|
onClick={() => openModal("NEW_LABEL")}
|
||||||
className="hover:bg-light-200 flex w-full items-center rounded-[5px] p-1.5 px-2 text-sm dark:hover:bg-dark-300"
|
className="flex w-full items-center rounded-[5px] p-1.5 px-2 text-sm hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
>
|
>
|
||||||
<HiMiniPlus size={22} className="pr-2" />
|
<HiMiniPlus size={22} className="pr-2" />
|
||||||
Create new label
|
Create new label
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
|
|
||||||
@@ -48,7 +46,7 @@ export default function ListSelector({
|
|||||||
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="border-light-200 hover:border-light-300 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] pl-2 text-left text-sm text-neutral-900 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:border-light-300 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
{selectedList?.name}
|
{selectedList?.name}
|
||||||
</Menu.Button>
|
</Menu.Button>
|
||||||
|
|
||||||
@@ -61,14 +59,14 @@ export default function ListSelector({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-600 absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
<Menu.Items className="absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
{lists?.map((list) => (
|
{lists?.map((list) => (
|
||||||
<Menu.Item key={list.publicId}>
|
<Menu.Item key={list.publicId}>
|
||||||
<div
|
<div
|
||||||
key={list.publicId}
|
key={list.publicId}
|
||||||
className="hover:bg-light-200 flex items-center rounded-[5px] p-2 dark:hover:bg-dark-300"
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await formik.setFieldValue(
|
await formik.setFieldValue(
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
import { Menu, Transition } from "@headlessui/react";
|
import { Menu, Transition } from "@headlessui/react";
|
||||||
import { HiMiniPlus } from "react-icons/hi2";
|
import { HiMiniPlus } from "react-icons/hi2";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -52,13 +50,13 @@ export default function LabelSelector({
|
|||||||
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="border-light-200 hover:bg-light-300 flex h-full w-full items-center rounded-[5px] border-[1px] pl-2 text-left text-sm text-neutral-900 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
<Menu.Button className="flex h-full w-full items-center rounded-[5px] border-[1px] border-light-200 pl-2 text-left text-sm text-neutral-900 hover:bg-light-300 dark:border-dark-100 dark:text-dark-1000 dark:hover:border-dark-300 dark:hover:bg-dark-200">
|
||||||
{selectedMembers.length ? (
|
{selectedMembers.length ? (
|
||||||
<div className="isolate flex -space-x-1 overflow-hidden">
|
<div className="isolate flex -space-x-1 overflow-hidden">
|
||||||
{selectedMembers.map((member) => (
|
{selectedMembers.map((member) => (
|
||||||
<span
|
<span
|
||||||
key={member.publicId}
|
key={member.publicId}
|
||||||
className="ring-light-200 relative z-30 inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-500 ring-1 dark:ring-dark-100"
|
className="relative z-30 inline-flex h-6 w-6 items-center justify-center rounded-full bg-gray-500 ring-1 ring-light-200 dark:ring-dark-100"
|
||||||
>
|
>
|
||||||
<span className="text-[10px] font-medium leading-none text-white">
|
<span className="text-[10px] font-medium leading-none text-white">
|
||||||
{member.user?.name
|
{member.user?.name
|
||||||
@@ -88,14 +86,14 @@ export default function LabelSelector({
|
|||||||
leaveFrom="transform opacity-100 scale-100"
|
leaveFrom="transform opacity-100 scale-100"
|
||||||
leaveTo="transform opacity-0 scale-95"
|
leaveTo="transform opacity-0 scale-95"
|
||||||
>
|
>
|
||||||
<Menu.Items className="bg-light-50 border-light-600 absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
<Menu.Items className="absolute right-[200px] top-[30px] z-10 mt-2 w-56 origin-top-right rounded-md border-[1px] border-light-600 bg-light-50 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:border-dark-500 dark:bg-dark-200">
|
||||||
<div className="p-2">
|
<div className="p-2">
|
||||||
<form onSubmit={formik.handleSubmit}>
|
<form onSubmit={formik.handleSubmit}>
|
||||||
{members?.map((member) => (
|
{members?.map((member) => (
|
||||||
<Menu.Item key={member.publicId}>
|
<Menu.Item key={member.publicId}>
|
||||||
<div
|
<div
|
||||||
key={member.publicId}
|
key={member.publicId}
|
||||||
className="hover:bg-light-200 flex items-center rounded-[5px] p-2 dark:hover:bg-dark-300"
|
className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-300"
|
||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
await formik.setFieldValue(
|
await formik.setFieldValue(
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { Fragment, useState } from "react";
|
import { Fragment, useState } from "react";
|
||||||
import { Listbox, Transition } from "@headlessui/react";
|
import { Listbox, Transition } from "@headlessui/react";
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
import { HiChevronUpDown, HiXMark } from "react-icons/hi2";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
import { Formik, Form, Field } from "formik";
|
import { Formik, Form, Field } from "formik";
|
||||||
|
|
||||||
@@ -58,7 +56,7 @@ export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
|||||||
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
|
<div className="flex w-full items-center justify-between pb-4 text-neutral-900 dark:text-dark-1000">
|
||||||
<h2 className="text-sm font-medium">New label</h2>
|
<h2 className="text-sm font-medium">New label</h2>
|
||||||
<button
|
<button
|
||||||
className="hover:bg-light-300 rounded p-1 focus:outline-none dark:hover:bg-dark-300"
|
className="rounded p-1 hover:bg-light-300 focus:outline-none dark:hover:bg-dark-300"
|
||||||
onClick={() => closeModal()}
|
onClick={() => closeModal()}
|
||||||
>
|
>
|
||||||
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
<HiXMark size={18} className="text-light-900 dark:text-dark-900" />
|
||||||
@@ -90,13 +88,13 @@ export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
|||||||
<Field
|
<Field
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
className="ring-light-600 focus:ring-light-600 block w-full rounded-md border-0 bg-white/5 py-1.5 text-neutral-900 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset dark:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
className="block w-full rounded-md border-0 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:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6"
|
||||||
/>
|
/>
|
||||||
<Listbox value={selected} onChange={setSelected}>
|
<Listbox value={selected} onChange={setSelected}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<>
|
<>
|
||||||
<div className="relative mt-4">
|
<div className="relative mt-4">
|
||||||
<Listbox.Button className="ring-light-600 focus:ring-light-600 block w-full rounded-md border-0 bg-white/5 px-4 py-1.5 shadow-sm ring-1 ring-inset focus:ring-2 focus:ring-inset dark:bg-dark-300 dark:text-dark-1000 dark:ring-dark-700 dark:focus:ring-dark-700 sm:text-sm sm:leading-6">
|
<Listbox.Button className="block w-full rounded-md border-0 bg-white/5 px-4 py-1.5 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">
|
||||||
<span className="flex items-center">
|
<span className="flex items-center">
|
||||||
<span
|
<span
|
||||||
style={{ backgroundColor: selected?.code }}
|
style={{ backgroundColor: selected?.code }}
|
||||||
@@ -121,7 +119,7 @@ export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
|||||||
leaveFrom="opacity-100"
|
leaveFrom="opacity-100"
|
||||||
leaveTo="opacity-0"
|
leaveTo="opacity-0"
|
||||||
>
|
>
|
||||||
<Listbox.Options className="bg-light-50 absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md py-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-dark-300 sm:text-sm">
|
<Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-light-50 py-2 text-base shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none dark:bg-dark-300 sm:text-sm">
|
||||||
{colours.map((colour, index) => (
|
{colours.map((colour, index) => (
|
||||||
<Listbox.Option
|
<Listbox.Option
|
||||||
key={`colours_${index}`}
|
key={`colours_${index}`}
|
||||||
@@ -130,7 +128,7 @@ export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
|||||||
>
|
>
|
||||||
{() => (
|
{() => (
|
||||||
<>
|
<>
|
||||||
<div className="hover:bg-light-200 flex items-center rounded-[5px] p-2 dark:hover:bg-dark-400">
|
<div className="flex items-center rounded-[5px] p-2 hover:bg-light-200 dark:hover:bg-dark-400">
|
||||||
<span
|
<span
|
||||||
style={{ backgroundColor: colour?.code }}
|
style={{ backgroundColor: colour?.code }}
|
||||||
className="ml-2 inline-block h-2 w-2 flex-shrink-0 rounded-full"
|
className="ml-2 inline-block h-2 w-2 flex-shrink-0 rounded-full"
|
||||||
@@ -153,7 +151,7 @@ export function NewLabelForm({ cardPublicId }: cardPublicId) {
|
|||||||
<div className="mt-5 sm:mt-6">
|
<div className="mt-5 sm:mt-6">
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-light-1000 text-light-50 inline-flex w-full justify-center rounded-md px-3 py-2 text-sm font-semibold shadow-sm focus-visible:outline-none dark:bg-dark-1000 dark:text-dark-50"
|
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 label
|
Create label
|
||||||
</button>
|
</button>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -12,12 +10,12 @@ import LabelSelector from "./components/LabelSelector";
|
|||||||
import ListSelector from "./components/ListSelector";
|
import ListSelector from "./components/ListSelector";
|
||||||
import MemberSelector from "./components/MemberSelector";
|
import MemberSelector from "./components/MemberSelector";
|
||||||
import { NewLabelForm } from "./components/NewLabelForm";
|
import { NewLabelForm } from "./components/NewLabelForm";
|
||||||
import { NewWorkspaceForm } from "~/app/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
|
|
||||||
import Modal from "~/app/components/modal";
|
import Modal from "~/components/modal";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
interface FormValues {
|
interface FormValues {
|
||||||
cardId: string;
|
cardId: string;
|
||||||
@@ -33,19 +31,21 @@ export default function CardPage() {
|
|||||||
? params.cardId[0]
|
? params.cardId[0]
|
||||||
: params?.cardId;
|
: params?.cardId;
|
||||||
|
|
||||||
const { data } = api.card.byId.useQuery({ id: cardId ?? "" });
|
const { data, error } = api.card.byId.useQuery({ id: cardId ?? "" });
|
||||||
|
|
||||||
const boardId = data?.list?.board?.publicId;
|
|
||||||
const labels = data?.list?.board?.labels;
|
|
||||||
const workspaceMembers = data?.list?.board?.workspace?.members;
|
|
||||||
const board = data?.list?.board;
|
const board = data?.list?.board;
|
||||||
|
const boardId = board?.publicId;
|
||||||
|
const labels = board?.labels;
|
||||||
|
const workspaceMembers = board?.workspace?.members;
|
||||||
const selectedLabels = data?.labels;
|
const selectedLabels = data?.labels;
|
||||||
const selectedMembers = data?.members;
|
const selectedMembers = data?.members;
|
||||||
|
|
||||||
|
console.log({ board: error });
|
||||||
|
|
||||||
const formattedLabels =
|
const formattedLabels =
|
||||||
labels?.map((label) => {
|
labels?.map((label) => {
|
||||||
const isSelected = selectedLabels?.some(
|
const isSelected = selectedLabels?.some(
|
||||||
(selectedLabel) => selectedLabel.label.publicId === label.publicId,
|
(selectedLabel) => selectedLabel.publicId === label.publicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -58,17 +58,18 @@ export default function CardPage() {
|
|||||||
const formattedLists =
|
const formattedLists =
|
||||||
board?.lists.map((list) => ({
|
board?.lists.map((list) => ({
|
||||||
...list,
|
...list,
|
||||||
selected: list.publicId === data?.list.publicId,
|
selected: list.publicId === data?.list?.publicId,
|
||||||
})) ?? [];
|
})) ?? [];
|
||||||
|
|
||||||
const formattedMembers =
|
const formattedMembers =
|
||||||
workspaceMembers?.map((member) => {
|
workspaceMembers?.map((member) => {
|
||||||
const isSelected = selectedMembers?.some(
|
const isSelected = selectedMembers?.some(
|
||||||
(assignedMember) => assignedMember.member.publicId === member.publicId,
|
(assignedMember) => assignedMember.publicId === member.publicId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...member,
|
...member,
|
||||||
|
user: member.user ?? { id: "", name: null },
|
||||||
selected: isSelected ?? false,
|
selected: isSelected ?? false,
|
||||||
};
|
};
|
||||||
}) ?? [];
|
}) ?? [];
|
||||||
@@ -98,14 +99,14 @@ export default function CardPage() {
|
|||||||
<div className="w-full p-8">
|
<div className="w-full p-8">
|
||||||
<div className="mb-8 flex w-full items-center justify-between">
|
<div className="mb-8 flex w-full items-center justify-between">
|
||||||
<Link
|
<Link
|
||||||
className="text-light-900 whitespace-nowrap font-medium leading-[2.3rem] tracking-tight dark:text-dark-900 sm:text-[1.2rem]"
|
className="whitespace-nowrap font-medium leading-[2.3rem] tracking-tight text-light-900 dark:text-dark-900 sm:text-[1.2rem]"
|
||||||
href={`/boards/${board?.publicId}`}
|
href={`/boards/${board?.publicId}`}
|
||||||
>
|
>
|
||||||
{board?.name}
|
{board?.name}
|
||||||
</Link>
|
</Link>
|
||||||
<IoChevronForwardSharp
|
<IoChevronForwardSharp
|
||||||
size={18}
|
size={18}
|
||||||
className="text-light-900 mx-2 dark:text-dark-900"
|
className="mx-2 text-light-900 dark:text-dark-900"
|
||||||
/>
|
/>
|
||||||
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
|
<form onSubmit={formik.handleSubmit} className="w-full space-y-6">
|
||||||
<div>
|
<div>
|
||||||
@@ -135,13 +136,13 @@ export default function CardPage() {
|
|||||||
formik.setFieldValue("description", e.target.value)
|
formik.setFieldValue("description", e.target.value)
|
||||||
}
|
}
|
||||||
onBlur={formik.submitForm}
|
onBlur={formik.submitForm}
|
||||||
className="text-light-900 block w-full border-0 bg-transparent py-1.5 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
className="block w-full border-0 bg-transparent py-1.5 text-light-900 focus-visible:outline-none dark:text-dark-1000 sm:text-sm sm:leading-6"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-light-200 border-light-600 text-light-900 min-w-[325px] border-l-[1px] p-8 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
|
<div className="min-w-[325px] border-l-[1px] border-light-600 bg-light-200 p-8 text-light-900 dark:border-dark-400 dark:bg-dark-100 dark:text-dark-900">
|
||||||
<div className="mb-4 flex w-full">
|
<div className="mb-4 flex w-full">
|
||||||
<p className="my-2 w-[100px] text-sm">List</p>
|
<p className="my-2 w-[100px] text-sm">List</p>
|
||||||
<ListSelector cardPublicId={cardId} lists={formattedLists} />
|
<ListSelector cardPublicId={cardId} lists={formattedLists} />
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
import { HiOutlinePlusSmall } from "react-icons/hi2";
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { useModal } from "~/providers/modal";
|
||||||
import { useWorkspace } from "~/app/providers/workspace";
|
import { useWorkspace } from "~/providers/workspace";
|
||||||
import Modal from "~/app/components/modal";
|
import Modal from "~/components/modal";
|
||||||
|
|
||||||
import { NewWorkspaceForm } from "~/app/components/NewWorkspaceForm";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
|
|
||||||
import { api } from "~/trpc/react";
|
import { api } from "~/utils/api";
|
||||||
|
|
||||||
export default function MembersPage() {
|
export default function MembersPage() {
|
||||||
const { openModal, modalContentType } = useModal();
|
const { modalContentType } = useModal();
|
||||||
const { workspace } = useWorkspace();
|
const { workspace } = useWorkspace();
|
||||||
|
|
||||||
const { data } = api.workspace.byId.useQuery(
|
const { data } = api.workspace.byId.useQuery(
|
||||||
@@ -27,7 +25,7 @@ export default function MembersPage() {
|
|||||||
<div className="flex">
|
<div className="flex">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="bg-light-1000 text-light-50 flex items-center gap-x-1.5 rounded-md px-3 py-2 text-sm font-semibold focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 dark:bg-dark-1000 dark:text-dark-50"
|
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("")}
|
// onClick={() => openModal("")}
|
||||||
>
|
>
|
||||||
<div className="h-5 w-5 items-center">
|
<div className="h-5 w-5 items-center">
|
||||||
@@ -45,32 +43,32 @@ export default function MembersPage() {
|
|||||||
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
<div className="-mx-4 -my-2 overflow-x-auto sm:-mx-6 lg:-mx-8">
|
||||||
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
<div className="inline-block min-w-full py-2 align-middle sm:px-6 lg:px-8">
|
||||||
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
<div className="overflow-hidden shadow ring-1 ring-black ring-opacity-5 sm:rounded-lg">
|
||||||
<table className="divide-light-600 min-w-full divide-y dark:divide-dark-600">
|
<table className="min-w-full divide-y divide-light-600 dark:divide-dark-600">
|
||||||
<thead className="bg-light-300 dark:bg-dark-200">
|
<thead className="bg-light-300 dark:bg-dark-200">
|
||||||
<tr>
|
<tr>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="text-light-900 py-3.5 pl-4 pr-3 text-left text-sm font-semibold dark:text-dark-900 sm:pl-6"
|
className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-light-900 dark:text-dark-900 sm:pl-6"
|
||||||
>
|
>
|
||||||
User
|
User
|
||||||
</th>
|
</th>
|
||||||
<th
|
<th
|
||||||
scope="col"
|
scope="col"
|
||||||
className="text-light-900 px-3 py-3.5 text-left text-sm font-semibold dark:text-dark-900"
|
className="px-3 py-3.5 text-left text-sm font-semibold text-light-900 dark:text-dark-900"
|
||||||
>
|
>
|
||||||
Role
|
Role
|
||||||
</th>
|
</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody className="bg-light-50 divide-light-600 divide-y dark:divide-dark-600 dark:bg-dark-100">
|
<tbody className="divide-y divide-light-600 bg-light-50 dark:divide-dark-600 dark:bg-dark-100">
|
||||||
{data?.members.map((member) => (
|
{data?.members.map((member) => (
|
||||||
<tr key={member.publicId}>
|
<tr key={member.publicId}>
|
||||||
<td>
|
<td>
|
||||||
<div className="flex items-center p-4">
|
<div className="flex items-center p-4">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="bg-light-1000 inline-flex h-9 w-9 items-center justify-center rounded-full dark:bg-dark-400">
|
<span className="inline-flex h-9 w-9 items-center justify-center rounded-full bg-light-1000 dark:bg-dark-400">
|
||||||
<span className="text-sm font-medium leading-none text-white">
|
<span className="text-sm font-medium leading-none text-white">
|
||||||
{member.user.name
|
{member.user?.name
|
||||||
?.split(" ")
|
?.split(" ")
|
||||||
.map((namePart) =>
|
.map((namePart) =>
|
||||||
namePart.charAt(0).toUpperCase(),
|
namePart.charAt(0).toUpperCase(),
|
||||||
@@ -83,11 +81,11 @@ export default function MembersPage() {
|
|||||||
<div>
|
<div>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="mr-2 text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
<p className="mr-2 text-sm font-medium text-neutral-900 dark:text-dark-1000">
|
||||||
{member.user.name}
|
{member.user?.name}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<p className="truncate text-sm text-dark-900">
|
<p className="truncate text-sm text-dark-900">
|
||||||
{member.user.email}
|
{member.user?.email}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
"use client";
|
import { useModal } from "~/providers/modal";
|
||||||
|
import Modal from "~/components/modal";
|
||||||
|
|
||||||
import { useModal } from "~/app/providers/modal";
|
import { NewWorkspaceForm } from "~/components/NewWorkspaceForm";
|
||||||
import Modal from "~/app/components/modal";
|
|
||||||
|
|
||||||
import { NewWorkspaceForm } from "~/app/components/NewWorkspaceForm";
|
|
||||||
|
|
||||||
export default function SettingsPage() {
|
export default function SettingsPage() {
|
||||||
const { openModal, modalContentType } = useModal();
|
const { modalContentType } = useModal();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="px-28 py-12">
|
<div className="px-28 py-12">
|
||||||
4
supabase/.gitignore
vendored
Normal file
4
supabase/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Supabase
|
||||||
|
.branches
|
||||||
|
.temp
|
||||||
|
.env
|
||||||
161
supabase/config.toml
Normal file
161
supabase/config.toml
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
# A string used to distinguish different Supabase projects on the same host. Defaults to the
|
||||||
|
# working directory name when running `supabase init`.
|
||||||
|
project_id = "kan"
|
||||||
|
|
||||||
|
[api]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the API URL.
|
||||||
|
port = 54321
|
||||||
|
# Schemas to expose in your API. Tables, views and stored procedures in this schema will get API
|
||||||
|
# endpoints. public and storage are always included.
|
||||||
|
schemas = ["public", "storage", "graphql_public"]
|
||||||
|
# Extra schemas to add to the search_path of every request. public is always included.
|
||||||
|
extra_search_path = ["public", "extensions"]
|
||||||
|
# The maximum number of rows returns from a view, table, or stored procedure. Limits payload size
|
||||||
|
# for accidental or malicious requests.
|
||||||
|
max_rows = 1000
|
||||||
|
|
||||||
|
[db]
|
||||||
|
# Port to use for the local database URL.
|
||||||
|
port = 54322
|
||||||
|
# Port used by db diff command to initialize the shadow database.
|
||||||
|
shadow_port = 54320
|
||||||
|
# The database major version to use. This has to be the same as your remote database's. Run `SHOW
|
||||||
|
# server_version;` on the remote database to check.
|
||||||
|
major_version = 15
|
||||||
|
|
||||||
|
[db.pooler]
|
||||||
|
enabled = false
|
||||||
|
# Port to use for the local connection pooler.
|
||||||
|
port = 54329
|
||||||
|
# Specifies when a server connection can be reused by other clients.
|
||||||
|
# Configure one of the supported pooler modes: `transaction`, `session`.
|
||||||
|
pool_mode = "transaction"
|
||||||
|
# How many server connections to allow per user/database pair.
|
||||||
|
default_pool_size = 20
|
||||||
|
# Maximum number of client connections allowed.
|
||||||
|
max_client_conn = 100
|
||||||
|
|
||||||
|
[realtime]
|
||||||
|
enabled = true
|
||||||
|
# Bind realtime via either IPv4 or IPv6. (default: IPv6)
|
||||||
|
# ip_version = "IPv6"
|
||||||
|
# The maximum length in bytes of HTTP request headers. (default: 4096)
|
||||||
|
# max_header_length = 4096
|
||||||
|
|
||||||
|
[studio]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for Supabase Studio.
|
||||||
|
port = 54323
|
||||||
|
# External URL of the API server that frontend connects to.
|
||||||
|
api_url = "http://127.0.0.1"
|
||||||
|
# OpenAI API Key to use for Supabase AI in the Supabase Studio.
|
||||||
|
openai_api_key = "env(OPENAI_API_KEY)"
|
||||||
|
|
||||||
|
# Email testing server. Emails sent with the local dev setup are not actually sent - rather, they
|
||||||
|
# are monitored, and you can view the emails that would have been sent from the web interface.
|
||||||
|
[inbucket]
|
||||||
|
enabled = true
|
||||||
|
# Port to use for the email testing server web interface.
|
||||||
|
port = 54324
|
||||||
|
# Uncomment to expose additional ports for testing user applications that send emails.
|
||||||
|
# smtp_port = 54325
|
||||||
|
# pop3_port = 54326
|
||||||
|
|
||||||
|
[storage]
|
||||||
|
enabled = true
|
||||||
|
# The maximum file size allowed (e.g. "5MB", "500KB").
|
||||||
|
file_size_limit = "50MiB"
|
||||||
|
|
||||||
|
[auth]
|
||||||
|
enabled = true
|
||||||
|
# The base URL of your website. Used as an allow-list for redirects and for constructing URLs used
|
||||||
|
# in emails.
|
||||||
|
site_url = "http://127.0.0.1:3000"
|
||||||
|
# A list of *exact* URLs that auth providers are permitted to redirect to post authentication.
|
||||||
|
additional_redirect_urls = ["https://127.0.0.1:3000"]
|
||||||
|
# How long tokens are valid for, in seconds. Defaults to 3600 (1 hour), maximum 604,800 (1 week).
|
||||||
|
jwt_expiry = 3600
|
||||||
|
# If disabled, the refresh token will never expire.
|
||||||
|
enable_refresh_token_rotation = true
|
||||||
|
# Allows refresh tokens to be reused after expiry, up to the specified interval in seconds.
|
||||||
|
# Requires enable_refresh_token_rotation = true.
|
||||||
|
refresh_token_reuse_interval = 10
|
||||||
|
# Allow/disallow new user signups to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# Allow/disallow testing manual linking of accounts
|
||||||
|
enable_manual_linking = false
|
||||||
|
|
||||||
|
[auth.email]
|
||||||
|
# Allow/disallow new user signups via email to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# If enabled, a user will be required to confirm any email change on both the old, and new email
|
||||||
|
# addresses. If disabled, only the new email is required to confirm.
|
||||||
|
double_confirm_changes = true
|
||||||
|
# If enabled, users need to confirm their email address before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
|
||||||
|
# Uncomment to customize email template
|
||||||
|
# [auth.email.template.invite]
|
||||||
|
# subject = "You have been invited"
|
||||||
|
# content_path = "./supabase/templates/invite.html"
|
||||||
|
|
||||||
|
[auth.sms]
|
||||||
|
# Allow/disallow new user signups via SMS to your project.
|
||||||
|
enable_signup = true
|
||||||
|
# If enabled, users need to confirm their phone number before signing in.
|
||||||
|
enable_confirmations = false
|
||||||
|
# Template for sending OTP to users
|
||||||
|
template = "Your code is {{ .Code }} ."
|
||||||
|
|
||||||
|
# Use pre-defined map of phone number to OTP for testing.
|
||||||
|
[auth.sms.test_otp]
|
||||||
|
# 4152127777 = "123456"
|
||||||
|
|
||||||
|
# This hook runs before a token is issued and allows you to add additional claims based on the authentication method used.
|
||||||
|
[auth.hook.custom_access_token]
|
||||||
|
# enabled = true
|
||||||
|
# uri = "pg-functions://<database>/<schema>/<hook_name>"
|
||||||
|
|
||||||
|
|
||||||
|
# Configure one of the supported SMS providers: `twilio`, `twilio_verify`, `messagebird`, `textlocal`, `vonage`.
|
||||||
|
[auth.sms.twilio]
|
||||||
|
enabled = false
|
||||||
|
account_sid = ""
|
||||||
|
message_service_sid = ""
|
||||||
|
# DO NOT commit your Twilio auth token to git. Use environment variable substitution instead:
|
||||||
|
auth_token = "env(SUPABASE_AUTH_SMS_TWILIO_AUTH_TOKEN)"
|
||||||
|
|
||||||
|
# Use an external OAuth provider. The full list of providers are: `apple`, `azure`, `bitbucket`,
|
||||||
|
# `discord`, `facebook`, `github`, `gitlab`, `google`, `keycloak`, `linkedin_oidc`, `notion`, `twitch`,
|
||||||
|
# `twitter`, `slack`, `spotify`, `workos`, `zoom`.
|
||||||
|
[auth.external.apple]
|
||||||
|
enabled = false
|
||||||
|
client_id = ""
|
||||||
|
# DO NOT commit your OAuth provider secret to git. Use environment variable substitution instead:
|
||||||
|
secret = "env(SUPABASE_AUTH_EXTERNAL_APPLE_SECRET)"
|
||||||
|
# Overrides the default auth redirectUrl.
|
||||||
|
redirect_uri = ""
|
||||||
|
# Overrides the default auth provider URL. Used to support self-hosted gitlab, single-tenant Azure,
|
||||||
|
# or any other third-party OIDC providers.
|
||||||
|
url = ""
|
||||||
|
|
||||||
|
[analytics]
|
||||||
|
enabled = false
|
||||||
|
port = 54327
|
||||||
|
vector_port = 54328
|
||||||
|
# Configure one of the supported backends: `postgres`, `bigquery`.
|
||||||
|
backend = "postgres"
|
||||||
|
|
||||||
|
# Experimental features may be deprecated any time
|
||||||
|
[experimental]
|
||||||
|
# Configures Postgres storage engine to use OrioleDB (S3)
|
||||||
|
orioledb_version = ""
|
||||||
|
# Configures S3 bucket URL, eg. <bucket_name>.s3-<region>.amazonaws.com
|
||||||
|
s3_host = "env(S3_HOST)"
|
||||||
|
# Configures S3 bucket region, eg. us-east-1
|
||||||
|
s3_region = "env(S3_REGION)"
|
||||||
|
# Configures AWS_ACCESS_KEY_ID for S3 bucket
|
||||||
|
s3_access_key = "env(S3_ACCESS_KEY)"
|
||||||
|
# Configures AWS_SECRET_ACCESS_KEY for S3 bucket
|
||||||
|
s3_secret_key = "env(S3_SECRET_KEY)"
|
||||||
68
supabase/seed.sql
Normal file
68
supabase/seed.sql
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
CREATE OR REPLACE FUNCTION reorder_lists(board_id BIGINT, list_id BIGINT, current_index INT, new_index INT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE SQL
|
||||||
|
AS $$
|
||||||
|
UPDATE list
|
||||||
|
SET index =
|
||||||
|
CASE
|
||||||
|
WHEN index = current_index AND id = list_id THEN new_index
|
||||||
|
WHEN current_index < new_index AND index > current_index AND index <= new_index THEN index - 1
|
||||||
|
WHEN current_index > new_index AND index >= new_index AND index < current_index THEN index + 1
|
||||||
|
ELSE index
|
||||||
|
END
|
||||||
|
WHERE "boardId" = board_id;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION reorder_cards(card_id BIGINT, current_list_id BIGINT, new_list_id BIGINT, current_index INT, new_index INT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE PLPGSQL
|
||||||
|
AS $$
|
||||||
|
DECLARE
|
||||||
|
card_index INT;
|
||||||
|
BEGIN
|
||||||
|
SELECT index INTO card_index FROM card WHERE "listId" = current_list_id AND id = card_id AND "deletedAt" IS NULL;
|
||||||
|
|
||||||
|
IF current_list_id = new_list_id THEN
|
||||||
|
UPDATE card
|
||||||
|
SET index =
|
||||||
|
CASE
|
||||||
|
WHEN index = current_index THEN new_index
|
||||||
|
WHEN current_index < new_index AND index > current_index AND index <= new_index THEN index - 1
|
||||||
|
WHEN current_index > new_index AND index >= new_index AND index < current_index THEN index + 1
|
||||||
|
ELSE index
|
||||||
|
END
|
||||||
|
WHERE "listId" = current_list_id AND "deletedAt" IS NULL;
|
||||||
|
ELSE
|
||||||
|
UPDATE card
|
||||||
|
SET index = index + 1
|
||||||
|
WHERE "listId" = new_list_id AND index >= new_index AND "deletedAt" IS NULL;
|
||||||
|
|
||||||
|
UPDATE card
|
||||||
|
SET index = index - 1
|
||||||
|
WHERE "listId" = current_list_id AND index >= current_index AND "deletedAt" IS NULL;
|
||||||
|
|
||||||
|
UPDATE card
|
||||||
|
SET "listId" = new_list_id, index = new_index
|
||||||
|
WHERE id = card_id AND "deletedAt" IS NULL;
|
||||||
|
END IF;
|
||||||
|
END;
|
||||||
|
$$
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shift_list_index(board_id BIGINT, list_index INT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE SQL
|
||||||
|
AS $$
|
||||||
|
UPDATE list
|
||||||
|
SET index = index - 1
|
||||||
|
WHERE "boardId" = board_id AND index > list_index AND "deletedAt" IS NULL;
|
||||||
|
$$;
|
||||||
|
|
||||||
|
CREATE OR REPLACE FUNCTION shift_card_index(list_id BIGINT, card_index INT)
|
||||||
|
RETURNS VOID
|
||||||
|
LANGUAGE SQL
|
||||||
|
AS $$
|
||||||
|
UPDATE card
|
||||||
|
SET index = index - 1
|
||||||
|
WHERE "listId" = list_id AND index > card_index AND "deletedAt" IS NULL;
|
||||||
|
$$;
|
||||||
|
|
||||||
@@ -1,26 +1,33 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "es2017",
|
/* Base Options: */
|
||||||
"lib": ["dom", "dom.iterable", "esnext"],
|
|
||||||
"allowJs": true,
|
|
||||||
"checkJs": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"noEmit": true,
|
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"module": "esnext",
|
"skipLibCheck": true,
|
||||||
"moduleResolution": "node",
|
"target": "es2022",
|
||||||
|
"allowJs": true,
|
||||||
"resolveJsonModule": true,
|
"resolveJsonModule": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"jsx": "preserve",
|
|
||||||
"incremental": true,
|
/* Strictness */
|
||||||
|
"strict": true,
|
||||||
"noUncheckedIndexedAccess": true,
|
"noUncheckedIndexedAccess": true,
|
||||||
|
"checkJs": true,
|
||||||
|
|
||||||
|
/* Bundled projects */
|
||||||
|
"lib": ["dom", "dom.iterable", "ES2022"],
|
||||||
|
"noEmit": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"jsx": "preserve",
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"incremental": true,
|
||||||
|
|
||||||
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"~/*": ["./src/*"]
|
"~/*": ["./src/*"]
|
||||||
},
|
}
|
||||||
"plugins": [{ "name": "next" }]
|
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
".eslintrc.cjs",
|
".eslintrc.cjs",
|
||||||
@@ -28,8 +35,11 @@
|
|||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
"**/*.cjs",
|
"**/*.cjs",
|
||||||
"**/*.mjs",
|
"**/*.js",
|
||||||
".next/types/**/*.ts"
|
".next/types/**/*.ts",
|
||||||
|
"src/env.mjs",
|
||||||
|
"types/**/*.d.ts"
|
||||||
],
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user