refactor: improve board manage page (#323)

* refactor: improve board manage page

* chore: address pull request feedback
This commit is contained in:
Meier Lukas
2024-04-13 12:42:03 +02:00
committed by GitHub
parent 6b1879cbb1
commit 9ed298d641
10 changed files with 200 additions and 60 deletions

View File

@@ -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<AuthProviderProps>) => {
return <SessionProvider session={session}>{children}</SessionProvider>;
};

View File

@@ -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 <AuthProvider session={session} {...innerProps} />;
},
(innerProps) => <JotaiProvider {...innerProps} />,
(innerProps) => <TRPCReactProvider {...innerProps} />,
(innerProps) => (

View File

@@ -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<RouterOutputs["board"]["getAll"][number], "id" | "name">;
}
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 (
<Menu.Dropdown>
<Menu.Item
component={Link}
href={`/boards/${board.name}/settings`}
leftSection={<IconSettings {...iconProps} />}
>
{t("settings.label")}
</Menu.Item>
<Menu.Divider />
<Menu.Label c="red.7">
{tCommon("menu.section.dangerZone.title")}
</Menu.Label>
<Menu.Item
c="red.7"
leftSection={<IconTrash {...iconProps} />}
onClick={handleDeletion}
disabled={isPending}
>
{t("delete.label")}
</Menu.Item>
</Menu.Dropdown>
);
};

View File

@@ -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")}
</Button>
);
};

View File

@@ -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 (
<Button onClick={onClick} loading={isPending} color="red">
{t("management.page.board.button.delete")}
</Button>
);
};

View File

@@ -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() {
<Grid>
{boards.map((board) => (
<GridCol span={{ xs: 12, md: 4 }} key={board.id}>
<Card>
<Text fw="bolder" tt="uppercase">
{board.name}
</Text>
<Text
size="sm"
my="md"
c="dimmed"
style={{ lineBreak: "anywhere" }}
>
{JSON.stringify(board)}
</Text>
<DeleteBoardButton id={board.id} />
</Card>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</>
);
}
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 (
<Card>
<CardSection p="sm" withBorder>
<Group justify="space-between" align="center">
<Group gap="sm">
<Tooltip label={t(`visibility.${visibility}`)}>
<VisibilityIcon size={20} stroke={1.5} />
</Tooltip>
<Text fw="bolder" tt="uppercase">
{board.name}
</Text>
</Group>
</Group>
</CardSection>
<CardSection p="sm">
<Group wrap="nowrap">
<Button
component={Link}
href={`/boards/${board.name}`}
variant="default"
fullWidth
>
{t("action.open.label")}
</Button>
<Menu position="bottom-end">
<MenuTarget>
<ActionIcon variant="default" size="lg">
<IconDotsVertical size={16} stroke={1.5} />
</ActionIcon>
</MenuTarget>
<BoardCardMenuDropdown board={board} />
</Menu>
</Group>
</CardSection>
</Card>
);
};

View File

@@ -57,5 +57,5 @@ export const AddBoardModal = createModal<InnerProps>(
);
},
).withOptions({
defaultTitle: (t) => t("management.page.board.button.create"),
defaultTitle: (t) => t("management.page.board.action.new.label"),
});

View File

@@ -52,6 +52,7 @@ export const boardRouter = createTRPCRouter({
columns: {
id: true,
name: true,
isPublic: true,
},
with: {
sections: {

View File

@@ -1 +1 @@
export { signIn, signOut } from "next-auth/react";
export { signIn, signOut, useSession, SessionProvider } from "next-auth/react";

View File

@@ -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: {