diff --git a/apps/web/src/components/SideNavigation.tsx b/apps/web/src/components/SideNavigation.tsx
index fe18ae7a..016013a6 100644
--- a/apps/web/src/components/SideNavigation.tsx
+++ b/apps/web/src/components/SideNavigation.tsx
@@ -2,6 +2,7 @@ import Link from "next/link";
import { useRouter } from "next/router";
import { Button } from "@headlessui/react";
import { t } from "@lingui/core/macro";
+import { useTheme } from "next-themes";
import { useEffect, useState } from "react";
import {
TbLayoutSidebarLeftCollapse,
@@ -15,10 +16,11 @@ import membersIconDark from "~/assets/members-dark.json";
import membersIconLight from "~/assets/members-light.json";
import settingsIconDark from "~/assets/settings-dark.json";
import settingsIconLight from "~/assets/settings-light.json";
+import templatesIconDark from "~/assets/templates-dark.json";
+import templatesIconLight from "~/assets/templates-light.json";
import ReactiveButton from "~/components/ReactiveButton";
import UserMenu from "~/components/UserMenu";
import WorkspaceMenu from "~/components/WorkspaceMenu";
-import { useTheme } from "next-themes";
interface SideNavigationProps {
user: UserType;
@@ -69,6 +71,11 @@ export default function SideNavigation({
href: "/boards",
icon: isDarkMode ? boardsIconDark : boardsIconLight,
},
+ {
+ name: t`Templates`,
+ href: "/templates",
+ icon: isDarkMode ? templatesIconDark : templatesIconLight,
+ },
{
name: t`Members`,
href: "/members",
diff --git a/apps/web/src/pages/templates/index.tsx b/apps/web/src/pages/templates/index.tsx
new file mode 100644
index 00000000..66218e5c
--- /dev/null
+++ b/apps/web/src/pages/templates/index.tsx
@@ -0,0 +1,17 @@
+import type { NextPageWithLayout } from "~/pages/_app";
+import { getDashboardLayout } from "~/components/Dashboard";
+import Popup from "~/components/Popup";
+import TemplatesView from "~/views/templates";
+
+const TemplatesPage: NextPageWithLayout = () => {
+ return (
+ <>
+
+
+ >
+ );
+};
+
+TemplatesPage.getLayout = (page) => getDashboardLayout(page);
+
+export default TemplatesPage;
diff --git a/apps/web/src/views/templates/index.tsx b/apps/web/src/views/templates/index.tsx
new file mode 100644
index 00000000..fb681e3c
--- /dev/null
+++ b/apps/web/src/views/templates/index.tsx
@@ -0,0 +1,40 @@
+import Link from "next/link";
+import { t } from "@lingui/core/macro";
+
+import { PageHead } from "~/components/PageHead";
+import { useWorkspace } from "~/providers/workspace";
+import { api } from "~/utils/api";
+
+export default function TemplatesView() {
+ const { workspace } = useWorkspace();
+ const { data: templates, isLoading } = api.board.templates.useQuery(
+ { workspacePublicId: workspace.publicId ?? "" },
+ { enabled: !!workspace.publicId },
+ );
+
+ return (
+
+
+
+
+ {t`Templates`}
+
+
+
+ {isLoading &&
{t`Loading templates...`}
}
+ {!isLoading && (templates?.length ?? 0) === 0 && (
+
{t`No templates yet`}
+ )}
+ {templates?.map((tpl) => (
+
+
{tpl.name}
+
+ ))}
+
+
+ );
+}
diff --git a/packages/api/src/routers/board.ts b/packages/api/src/routers/board.ts
index 61679c89..f30c8a60 100644
--- a/packages/api/src/routers/board.ts
+++ b/packages/api/src/routers/board.ts
@@ -14,6 +14,51 @@ import { createTRPCRouter, protectedProcedure, publicProcedure } from "../trpc";
import { assertUserInWorkspace } from "../utils/auth";
export const boardRouter = createTRPCRouter({
+ templates: protectedProcedure
+ .meta({
+ openapi: {
+ method: "GET",
+ path: "/workspaces/{workspacePublicId}/templates",
+ summary: "Get templates",
+ description: "Retrieves all templates for a given workspace",
+ tags: ["Boards"],
+ protect: true,
+ },
+ })
+ .input(z.object({ workspacePublicId: z.string().min(12) }))
+ .output(
+ z.custom<
+ Awaited>
+ >(),
+ )
+ .query(async ({ ctx, input }) => {
+ const userId = ctx.user?.id;
+
+ if (!userId)
+ throw new TRPCError({
+ message: `User not authenticated`,
+ code: "UNAUTHORIZED",
+ });
+
+ const workspace = await workspaceRepo.getByPublicId(
+ ctx.db,
+ input.workspacePublicId,
+ );
+
+ if (!workspace)
+ throw new TRPCError({
+ message: `Workspace with public ID ${input.workspacePublicId} not found`,
+ code: "NOT_FOUND",
+ });
+
+ await assertUserInWorkspace(ctx.db, userId, workspace.id);
+
+ const result = boardRepo.getAllByWorkspaceId(ctx.db, workspace.id, {
+ type: "template",
+ });
+
+ return result;
+ }),
all: protectedProcedure
.meta({
openapi: {
@@ -225,7 +270,7 @@ export const boardRouter = createTRPCRouter({
code: "INTERNAL_SERVER_ERROR",
});
- if (input.lists.length) {
+ if (input.lists?.length) {
const listInputs = input.lists.map((list, index) => ({
publicId: generateUID(),
name: list,
@@ -237,7 +282,7 @@ export const boardRouter = createTRPCRouter({
await listRepo.bulkCreate(ctx.db, listInputs);
}
- if (input.labels.length) {
+ if (input.labels?.length) {
const labelInputs = input.labels.map((label, index) => ({
publicId: generateUID(),
name: label,