diff --git a/apps/nextjs/src/app/[locale]/_client-providers/session.tsx b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx new file mode 100644 index 000000000..5ed469e1c --- /dev/null +++ b/apps/nextjs/src/app/[locale]/_client-providers/session.tsx @@ -0,0 +1,17 @@ +"use client"; + +import type { PropsWithChildren } from "react"; + +import type { Session } from "@homarr/auth"; +import { SessionProvider } from "@homarr/auth/client"; + +interface AuthProviderProps { + session: Session | null; +} + +export const AuthProvider = ({ + children, + session, +}: PropsWithChildren) => { + return {children}; +}; diff --git a/apps/nextjs/src/app/[locale]/layout.tsx b/apps/nextjs/src/app/[locale]/layout.tsx index d151f5005..67b1b7e4d 100644 --- a/apps/nextjs/src/app/[locale]/layout.tsx +++ b/apps/nextjs/src/app/[locale]/layout.tsx @@ -5,12 +5,14 @@ import "@homarr/notifications/styles.css"; import "@homarr/spotlight/styles.css"; import "@homarr/ui/styles.css"; +import { auth } from "@homarr/auth"; import { ModalProvider } from "@homarr/modals"; import { Notifications } from "@homarr/notifications"; import { ColorSchemeScript, createTheme, MantineProvider } from "@homarr/ui"; import { JotaiProvider } from "./_client-providers/jotai"; import { NextInternationalProvider } from "./_client-providers/next-international"; +import { AuthProvider } from "./_client-providers/session"; import { TRPCReactProvider } from "./_client-providers/trpc"; import { composeWrappers } from "./compose"; @@ -52,6 +54,10 @@ export default function Layout(props: { const colorScheme = "dark"; const StackedProvider = composeWrappers([ + async (innerProps) => { + const session = await auth(); + return ; + }, (innerProps) => , (innerProps) => , (innerProps) => ( diff --git a/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx b/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx new file mode 100644 index 000000000..106189c64 --- /dev/null +++ b/apps/nextjs/src/app/[locale]/manage/boards/_components/board-card-menu-dropdown.tsx @@ -0,0 +1,75 @@ +"use client"; + +import { useCallback } from "react"; +import Link from "next/link"; + +import type { RouterOutputs } from "@homarr/api"; +import { clientApi } from "@homarr/api/client"; +import { useConfirmModal } from "@homarr/modals"; +import { useScopedI18n } from "@homarr/translation/client"; +import { IconSettings, IconTrash, Menu } from "@homarr/ui"; + +import { revalidatePathAction } from "~/app/revalidatePathAction"; + +const iconProps = { + size: 16, + stroke: 1.5, +}; + +interface BoardCardMenuDropdownProps { + board: Pick; +} + +export const BoardCardMenuDropdown = ({ + board, +}: BoardCardMenuDropdownProps) => { + const t = useScopedI18n("management.page.board.action"); + const tCommon = useScopedI18n("common"); + + const { openConfirmModal } = useConfirmModal(); + + const { mutateAsync, isPending } = clientApi.board.delete.useMutation({ + onSettled: async () => { + await revalidatePathAction("/manage/boards"); + }, + }); + + const handleDeletion = useCallback(() => { + openConfirmModal({ + title: t("delete.confirm.title"), + children: t("delete.confirm.description", { + name: board.name, + }), + onConfirm: async () => { + await mutateAsync({ + id: board.id, + }); + }, + }); + }, [board.id, board.name, mutateAsync, openConfirmModal, t]); + + return ( + + } + > + {t("settings.label")} + + + + + {tCommon("menu.section.dangerZone.title")} + + } + onClick={handleDeletion} + disabled={isPending} + > + {t("delete.label")} + + + ); +}; 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 index a9f60bb10..7fce4f975 100644 --- 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 @@ -41,7 +41,7 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => { onClick={onClick} loading={isPending} > - {t("management.page.board.button.create")} + {t("management.page.board.action.new.label")} ); }; 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 deleted file mode 100644 index 4d49ed17a..000000000 --- a/apps/nextjs/src/app/[locale]/manage/boards/_components/delete-board-button.tsx +++ /dev/null @@ -1,34 +0,0 @@ -"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 index dfebada4b..4979c45ca 100644 --- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx @@ -1,11 +1,28 @@ -import React from "react"; +import Link from "next/link"; +import type { RouterOutputs } from "@homarr/api"; import { api } from "@homarr/api/server"; import { getScopedI18n } from "@homarr/translation/server"; -import { Card, Grid, GridCol, Group, Text, Title } from "@homarr/ui"; +import { + ActionIcon, + Button, + Card, + CardSection, + Grid, + GridCol, + Group, + IconDotsVertical, + IconLock, + IconWorld, + Menu, + MenuTarget, + Text, + Title, + Tooltip, +} from "@homarr/ui"; +import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown"; 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"); @@ -22,25 +39,58 @@ export default async function ManageBoardsPage() { {boards.map((board) => ( - - - {board.name} - - - - {JSON.stringify(board)} - - - - + ))} ); } + +interface BoardCardProps { + board: RouterOutputs["board"]["getAll"][number]; +} + +const BoardCard = async ({ board }: BoardCardProps) => { + const t = await getScopedI18n("management.page.board"); + const visibility = board.isPublic ? "public" : "private"; + const VisibilityIcon = board.isPublic ? IconWorld : IconLock; + + return ( + + + + + + + + + {board.name} + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/apps/nextjs/src/components/manage/boards/add-board-modal.tsx b/apps/nextjs/src/components/manage/boards/add-board-modal.tsx index 31ff38cad..0966e45d9 100644 --- a/apps/nextjs/src/components/manage/boards/add-board-modal.tsx +++ b/apps/nextjs/src/components/manage/boards/add-board-modal.tsx @@ -57,5 +57,5 @@ export const AddBoardModal = createModal( ); }, ).withOptions({ - defaultTitle: (t) => t("management.page.board.button.create"), + defaultTitle: (t) => t("management.page.board.action.new.label"), }); diff --git a/packages/api/src/router/board.ts b/packages/api/src/router/board.ts index 4cd92665d..c6c69e93d 100644 --- a/packages/api/src/router/board.ts +++ b/packages/api/src/router/board.ts @@ -52,6 +52,7 @@ export const boardRouter = createTRPCRouter({ columns: { id: true, name: true, + isPublic: true, }, with: { sections: { diff --git a/packages/auth/client.ts b/packages/auth/client.ts index 0fbefd91b..ab914304f 100644 --- a/packages/auth/client.ts +++ b/packages/auth/client.ts @@ -1 +1 @@ -export { signIn, signOut } from "next-auth/react"; +export { signIn, signOut, useSession, SessionProvider } from "next-auth/react"; diff --git a/packages/translation/src/lang/en.ts b/packages/translation/src/lang/en.ts index fd1c2f1b3..ef24d4bf7 100644 --- a/packages/translation/src/lang/en.ts +++ b/packages/translation/src/lang/en.ts @@ -234,6 +234,13 @@ export default { navigateDefaultBoard: "Navigate to default board", }, }, + menu: { + section: { + dangerZone: { + title: "Danger Zone", + }, + }, + }, noResults: "No results found", preview: { show: "Show preview", @@ -816,10 +823,28 @@ export default { }, page: { board: { - title: "Manage boards", - button: { - create: "Create board", - delete: "Delete board", + title: "Your boards", + action: { + new: { + label: "New board", + }, + open: { + label: "Open board", + }, + settings: { + label: "Settings", + }, + delete: { + label: "Delete permanently", + confirm: { + title: "Delete board", + description: "Are you sure you want to delete the {name} board?", + }, + }, + }, + visibility: { + public: "This board is public", + private: "This board is private", }, modal: { createBoard: {