diff --git a/.gitignore b/.gitignore
index a9669defa..43091d08b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@
node_modules
.pnp
.pnp.js
+.idea/
# testing
coverage
diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx
new file mode 100644
index 000000000..339ae07d8
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/create-board-button.tsx
@@ -0,0 +1,28 @@
+"use client";
+
+import React from "react";
+
+import { clientApi } from "@homarr/api/client";
+import { useI18n } from "@homarr/translation/client";
+import { Button } from "@homarr/ui";
+
+import { revalidatePathAction } from "~/app/revalidatePathAction";
+
+export const CreateBoardButton = () => {
+ const t = useI18n();
+ const { mutateAsync, isPending } = clientApi.board.create.useMutation({
+ onSettled: async () => {
+ await revalidatePathAction("/manage/boards");
+ },
+ });
+
+ const onClick = React.useCallback(async () => {
+ await mutateAsync({ name: "default" });
+ }, [mutateAsync]);
+
+ return (
+
+ );
+};
diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx
new file mode 100644
index 000000000..4d49ed17a
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx
@@ -0,0 +1,34 @@
+"use client";
+
+import React from "react";
+
+import { clientApi } from "@homarr/api/client";
+import { useI18n } from "@homarr/translation/client";
+import { Button } from "@homarr/ui";
+
+import { revalidatePathAction } from "~/app/revalidatePathAction";
+
+interface Props {
+ id: string;
+}
+
+export const DeleteBoardButton = ({ id }: Props) => {
+ const t = useI18n();
+ const { mutateAsync, isPending } = clientApi.board.delete.useMutation({
+ onSettled: async () => {
+ await revalidatePathAction("/manage/boards");
+ },
+ });
+
+ const onClick = React.useCallback(async () => {
+ await mutateAsync({
+ id,
+ });
+ }, [id, mutateAsync]);
+
+ return (
+
+ );
+};
diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
new file mode 100644
index 000000000..a5baaa881
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
@@ -0,0 +1,38 @@
+import React from "react";
+
+import { getScopedI18n } from "@homarr/translation/server";
+import { Card, Grid, GridCol, Text, Title } from "@homarr/ui";
+
+import { api } from "~/trpc/server";
+import { CreateBoardButton } from "./_components/create-board-button";
+import { DeleteBoardButton } from "./_components/delete-board-button";
+
+export default async function ManageBoardsPage() {
+ const t = await getScopedI18n("management.page.board");
+
+ const boards = await api.board.getAll.query();
+
+ return (
+ <>
+
{t("title")}
+
+
+
+
+ {boards.map((board) => (
+
+
+ {board.name}
+
+
+ {JSON.stringify(board)}
+
+
+
+
+
+ ))}
+
+ >
+ );
+}
diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts
index 1560e4f0b..246b78201 100644
--- a/packages/api/src/router/board.ts
+++ b/packages/api/src/router/board.ts
@@ -2,7 +2,7 @@ import { TRPCError } from "@trpc/server";
import superjson from "superjson";
import type { Database } from "@homarr/db";
-import { and, db, eq, inArray } from "@homarr/db";
+import { and, createId, db, eq, inArray } from "@homarr/db";
import {
boards,
integrationItems,
@@ -47,6 +47,43 @@ const filterUpdatedItems = (
);
export const boardRouter = createTRPCRouter({
+ getAll: publicProcedure.query(async () => {
+ return await db.query.boards.findMany({
+ columns: {
+ id: true,
+ name: true,
+ },
+ with: {
+ sections: {
+ with: {
+ items: true,
+ },
+ },
+ },
+ });
+ }),
+ create: publicProcedure
+ .input(validation.board.create)
+ .mutation(async ({ ctx, input }) => {
+ const boardId = createId();
+ await ctx.db.transaction(async (transaction) => {
+ await transaction.insert(boards).values({
+ id: boardId,
+ name: input.name,
+ });
+ await transaction.insert(sections).values({
+ id: createId(),
+ kind: "empty",
+ position: 0,
+ boardId,
+ });
+ });
+ }),
+ delete: publicProcedure
+ .input(z.object({ id: z.string() }))
+ .mutation(async ({ ctx, input }) => {
+ await ctx.db.delete(boards).where(eq(boards.id, input.id));
+ }),
default: publicProcedure.query(async ({ ctx }) => {
return await getFullBoardByName(ctx.db, "default");
}),
diff --git a/packages/db/schema/sqlite.ts b/packages/db/schema/sqlite.ts
index f9fd3da8e..a154cf4f7 100644
--- a/packages/db/schema/sqlite.ts
+++ b/packages/db/schema/sqlite.ts
@@ -116,7 +116,7 @@ export const integrationSecrets = sqliteTable(
export const boards = sqliteTable("board", {
id: text("id").notNull().primaryKey(),
- name: text("name").notNull(),
+ name: text("name").unique().notNull(),
isPublic: int("is_public", { mode: "boolean" }).default(false).notNull(),
pageTitle: text("page_title"),
metaTitle: text("meta_title"),
diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts
index 71cb4c950..62acddd59 100644
--- a/packages/translation/src/lang/en.ts
+++ b/packages/translation/src/lang/en.ts
@@ -363,5 +363,14 @@ export default {
about: "About",
},
},
+ page: {
+ board: {
+ title: "Manage boards",
+ button: {
+ create: "Create board",
+ delete: "Delete board",
+ },
+ },
+ },
},
} as const;
diff --git a/packages/validation/src/board.ts b/packages/validation/src/board.ts
index dc2a93989..6b56666fb 100644
--- a/packages/validation/src/board.ts
+++ b/packages/validation/src/board.ts
@@ -36,8 +36,11 @@ const saveSchema = z.object({
sections: z.array(createSectionSchema(commonItemSchema)),
});
+const createSchema = z.object({ name: z.string() });
+
export const boardSchemas = {
byName: byNameSchema,
saveGeneralSettings: saveGeneralSettingsSchema,
save: saveSchema,
+ create: createSchema,
};