diff --git a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx
index 27ab01fb5..25fa5ac9e 100644
--- a/apps/nextjs/src/app/[locale]/manage/apps/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/apps/page.tsx
@@ -1,6 +1,6 @@
import { Fragment } from "react";
import { redirect } from "next/navigation";
-import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
+import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core";
import { IconBox, IconPencil } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -11,9 +11,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
-import { ManageContainer } from "~/components/manage/manage-container";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { AppDeleteButton } from "./_app-delete-button";
@@ -39,34 +38,31 @@ export default async function AppsPage(props: AppsPageProps) {
const { items: apps, totalCount } = await api.app.getPaginated(searchParams);
const t = await getScopedI18n("app");
- return (
-
-
-
- {t("page.list.title")}
-
-
- {session.user.permissions.includes("app-create") && (
-
- {t("page.create.title")}
-
- )}
-
- {apps.length === 0 && }
- {apps.length > 0 && (
-
- {apps.map((app) => (
-
- ))}
-
- )}
+ const canCreate = session.user.permissions.includes("app-create");
- {/* Added margin to not hide pagination behind affix-button */}
-
-
-
-
-
+ return (
+
+ {t("page.create.title")}
+
+ ) : undefined
+ }
+ toolbar={}
+ footer={}
+ floatingPrimaryAction={canCreate}
+ >
+ {apps.length === 0 && }
+ {apps.length > 0 && (
+
+ {apps.map((app) => (
+
+ ))}
+
+ )}
+
);
}
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 849db72e7..5e9642e32 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
@@ -1,43 +1,38 @@
"use client";
-import { Affix, Button, Menu } from "@mantine/core";
+import { Button, Menu } from "@mantine/core";
import { IconCategoryPlus, IconChevronDown, IconFileImport } from "@tabler/icons-react";
import { useModalAction } from "@homarr/modals";
import { AddBoardModal, ImportBoardModal } from "@homarr/modals-collection";
import { useI18n } from "@homarr/translation/client";
+import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
+
export const CreateBoardButton = () => {
const t = useI18n();
const { openModal: openAddModal } = useModalAction(AddBoardModal);
const { openModal: openImportModal } = useModalAction(ImportBoardModal);
- const buttonGroupContent = (
- <>
- } onClick={openAddModal}>
- {t("management.page.board.action.new.label")}
-
-
- >
- );
-
return (
- <>
- {buttonGroupContent}
-
- {buttonGroupContent}
-
- >
+
+
+ } onClick={openAddModal}>
+ {t("management.page.board.action.new.label")}
+
+
+
+
);
};
diff --git a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
index 9c0a28344..796c6e5c0 100644
--- a/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/boards/page.tsx
@@ -9,12 +9,17 @@ import {
Group,
Menu,
MenuTarget,
- Stack,
Text,
- Title,
Tooltip,
} from "@mantine/core";
-import { IconDeviceMobile, IconDotsVertical, IconHomeFilled, IconLock, IconWorld } from "@tabler/icons-react";
+import {
+ IconDeviceMobile,
+ IconDotsVertical,
+ IconHomeFilled,
+ IconLayoutDashboard,
+ IconLock,
+ IconWorld,
+} from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -23,8 +28,8 @@ import { getScopedI18n } from "@homarr/translation/server";
import { Link, UserAvatar } from "@homarr/ui";
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
-import { ManageContainer } from "~/components/manage/manage-container";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
+import { NoResults } from "~/components/no-results";
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
import { CreateBoardButton } from "./_components/create-board-button";
@@ -35,23 +40,22 @@ export default async function ManageBoardsPage() {
const canCreateBoards = session?.user.permissions.includes("board-create");
return (
-
-
-
-
- {t("title")}
- {canCreateBoards && }
-
-
-
+ : undefined}
+ floatingPrimaryAction={canCreateBoards}
+ >
+ {boards.length === 0 && }
+ {boards.length > 0 && (
+
{boards.map((board) => (
))}
-
-
+ )}
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx
index 1fe3a83e2..994d63b3c 100644
--- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx
@@ -7,9 +7,7 @@ import {
AccordionPanel,
ActionIcon,
ActionIconGroup,
- Affix,
Anchor,
- Box,
Button,
Divider,
Group,
@@ -24,9 +22,8 @@ import {
TableThead,
TableTr,
Text,
- Title,
} from "@mantine/core";
-import { IconChevronDown, IconChevronUp, IconPencil, IconPlugX } from "@tabler/icons-react";
+import { IconChevronDown, IconPencil, IconPlugX } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -37,8 +34,8 @@ import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { CountBadge, IntegrationAvatar, Link } from "@homarr/ui";
-import { ManageContainer } from "~/components/manage/manage-container";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { NoResults } from "~/components/no-results";
import { env } from "~/env";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
@@ -66,38 +63,23 @@ export default async function IntegrationsPage(props: IntegrationsPageProps) {
const canCreateIntegrations = session.user.permissions.includes("integration-create");
return (
-
-
-
-
- {t("page.list.title")}
-
- {canCreateIntegrations && (
- <>
-
-
-
-
- }>{t("action.create")}
-
-
-
-
-
-
-
-
- }>{t("action.create")}
-
-
-
- >
- )}
-
-
-
-
-
+
+
+
+ }>{t("action.create")}
+
+
+
+ ) : undefined
+ }
+ floatingPrimaryAction={canCreateIntegrations}
+ >
+
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
index b14ceb531..51d4a2caf 100644
--- a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx
@@ -4,17 +4,15 @@ import {
Anchor,
Group,
Image,
- Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
- Title,
Tooltip,
} from "@mantine/core";
-import { IconExternalLink } from "@tabler/icons-react";
+import { IconExternalLink, IconPhoto } from "@tabler/icons-react";
import { z } from "zod/v4";
import type { RouterOutputs } from "@homarr/api";
@@ -26,8 +24,9 @@ import { createLocalImageUrl } from "@homarr/icons/local";
import { getI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination, UserAvatar } from "@homarr/ui";
-import { ManageContainer } from "~/components/manage/manage-container";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
+import { NoResults } from "~/components/no-results";
import { CopyMedia } from "./_actions/copy-media";
import { DeleteMedia } from "./_actions/delete-media";
import { IncludeFromAllUsersSwitch } from "./_actions/show-all";
@@ -48,7 +47,7 @@ interface MediaListPageProps {
searchParams: Promise>;
}
-export default async function GroupsListPage(props: MediaListPageProps) {
+export default async function MediaListPage(props: MediaListPageProps) {
const session = await auth();
if (!session) {
@@ -58,22 +57,31 @@ export default async function GroupsListPage(props: MediaListPageProps) {
const t = await getI18n();
const searchParams = searchParamsSchema.parse(await props.searchParams);
const { items: medias, totalCount } = await api.media.getPaginated(searchParams);
+ const canUpload = session.user.permissions.includes("media-upload");
return (
-
-
-
- {t("media.plural")}
-
-
-
- {session.user.permissions.includes("media-view-all") && (
-
- )}
-
-
- {session.user.permissions.includes("media-upload") && }
+
+
+
+ ) : undefined
+ }
+ toolbar={
+
+
+ {session.user.permissions.includes("media-view-all") && (
+
+ )}
+ }
+ footer={}
+ floatingPrimaryAction={canUpload}
+ >
+ {medias.length === 0 && }
+ {medias.length > 0 && (
@@ -90,13 +98,8 @@ export default async function GroupsListPage(props: MediaListPageProps) {
))}
-
- {/* Added margin to not hide pagination behind affix-button */}
-
-
-
-
-
+ )}
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx
index ce2500d02..00ca08055 100644
--- a/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/search-engines/page.tsx
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation";
-import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
+import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text } from "@mantine/core";
import { IconPencil, IconSearch } from "@tabler/icons-react";
import { z } from "zod/v4";
@@ -10,9 +10,8 @@ import type { inferSearchParamsFromSchema } from "@homarr/common/types";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { Link, SearchInput, TablePagination } from "@homarr/ui";
-import { ManageContainer } from "~/components/manage/manage-container";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
import { NoResults } from "~/components/no-results";
import { SearchEngineDeleteButton } from "./_search-engine-delete-button";
@@ -38,34 +37,31 @@ export default async function SearchEnginesPage(props: SearchEnginesPageProps) {
const tEngine = await getScopedI18n("search.engine");
- return (
-
-
-
- {tEngine("page.list.title")}
-
-
- {session.user.permissions.includes("search-engine-create") && (
-
- {tEngine("page.create.title")}
-
- )}
-
- {searchEngines.length === 0 && }
- {searchEngines.length > 0 && (
-
- {searchEngines.map((searchEngine) => (
-
- ))}
-
- )}
+ const canCreate = session.user.permissions.includes("search-engine-create");
- {/* Added margin to not hide pagination behind affix-button */}
-
-
-
-
-
+ return (
+
+ {tEngine("page.create.title")}
+
+ ) : undefined
+ }
+ toolbar={}
+ footer={}
+ floatingPrimaryAction={canCreate}
+ >
+ {searchEngines.length === 0 && }
+ {searchEngines.length > 0 && (
+
+ {searchEngines.map((searchEngine) => (
+
+ ))}
+
+ )}
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.tsx b/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.tsx
index 22f4d1b88..0e0fce5b9 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/_components/user-list.tsx
@@ -1,24 +1,22 @@
"use client";
import { useMemo } from "react";
-import { Anchor, Button, Group, Text, Title, Tooltip } from "@mantine/core";
+import { Anchor, Group, Text, Tooltip } from "@mantine/core";
import { IconCheck } from "@tabler/icons-react";
import type { MRT_ColumnDef } from "mantine-react-table";
import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
-import { useI18n, useScopedI18n } from "@homarr/translation/client";
+import { useI18n } from "@homarr/translation/client";
import { Link, UserAvatar } from "@homarr/ui";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
interface UserListComponentProps {
initialUserList: RouterOutputs["user"]["getAll"];
- credentialsProviderEnabled: boolean;
}
-export const UserListComponent = ({ initialUserList, credentialsProviderEnabled }: UserListComponentProps) => {
- const tUserList = useScopedI18n("management.page.user.list");
+export const UserListComponent = ({ initialUserList }: UserListComponentProps) => {
const t = useI18n();
const { data, isLoading } = clientApi.user.getAll.useQuery(undefined, {
initialData: initialUserList,
@@ -69,21 +67,10 @@ export const UserListComponent = ({ initialUserList, credentialsProviderEnabled
enableFullScreenToggle: false,
layoutMode: "grid-no-grow",
getRowId: (row) => row.id,
- renderTopToolbarCustomActions: () =>
- credentialsProviderEnabled ? (
-
- ) : null,
state: {
isLoading,
},
});
- return (
- <>
- {tUserList("title")}
-
- >
- );
+ return ;
};
diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx
index 7ca382bff..353bc06ea 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/groups/_client.tsx
@@ -1,7 +1,7 @@
"use client";
-import { useCallback, useMemo, useState } from "react";
-import { Group, TextInput } from "@mantine/core";
+import { useMemo, useState } from "react";
+import { TextInput } from "@mantine/core";
import { IconSearch } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
@@ -33,29 +33,20 @@ export const GroupsList = ({ groups }: GroupsListProps) => {
return (
<>
-
- }
- value={search}
- onChange={(event) => setSearch(event.currentTarget.value)}
- placeholder={`${t("group.search")}...`}
- style={{ flex: 1 }}
- />
-
-
-
+ }
+ value={search}
+ onChange={(event) => setSearch(event.currentTarget.value)}
+ placeholder={`${t("group.search")}...`}
+ />
>
);
};
-const AddGroup = () => {
+export const AddGroupButton = () => {
const t = useI18n();
const { openModal } = useModalAction(AddGroupModal);
- const handleAddGroup = useCallback(() => {
- openModal();
- }, [openModal]);
-
- return {t("group.action.create.label")};
+ return openModal()}>{t("group.action.create.label")};
};
diff --git a/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx
index 19ec73ff7..48f6c8f8f 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/groups/page.tsx
@@ -1,5 +1,5 @@
import { notFound } from "next/navigation";
-import { Card, Group, Stack, Text, ThemeIcon, Title, UnstyledButton } from "@mantine/core";
+import { Card, Group, Stack, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
import { IconChevronRight, IconUsersGroup } from "@tabler/icons-react";
import { api } from "@homarr/api/server";
@@ -8,9 +8,8 @@ import { everyoneGroup } from "@homarr/definitions";
import { getI18n } from "@homarr/translation/server";
import { Link } from "@homarr/ui";
-import { ManageContainer } from "~/components/manage/manage-container";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
-import { GroupsList } from "./_client";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
+import { AddGroupButton, GroupsList } from "./_client";
import classes from "./groups.module.css";
export default async function GroupsListPage() {
@@ -26,11 +25,8 @@ export default async function GroupsListPage() {
const groupsWithoutEveryone = groups.filter((group) => group.name !== everyoneGroup);
return (
-
-
+ } floatingPrimaryAction>
- {t("group.title")}
-
{dbEveryoneGroup && (
@@ -53,6 +49,6 @@ export default async function GroupsListPage() {
-
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-create-button.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-create-button.tsx
new file mode 100644
index 000000000..01c3f40b2
--- /dev/null
+++ b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-create-button.tsx
@@ -0,0 +1,20 @@
+"use client";
+
+import { Button } from "@mantine/core";
+
+import { useModalAction } from "@homarr/modals";
+import { InviteCreateModal } from "@homarr/modals-collection";
+import { useScopedI18n } from "@homarr/translation/client";
+
+import { MANAGE_ACTION_BUTTON_MIN_WIDTH } from "~/components/manage/manage-page.constants";
+
+export const InviteCreateButton = () => {
+ const t = useScopedI18n("management.page.user.invite");
+ const { openModal } = useModalAction(InviteCreateModal);
+
+ return (
+
+ );
+};
diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx
index 5fa1b5eff..4bc1eaff1 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/invites/_components/invite-list.tsx
@@ -1,7 +1,7 @@
"use client";
import { useCallback, useMemo } from "react";
-import { ActionIcon, Button, Title } from "@mantine/core";
+import { ActionIcon } from "@mantine/core";
import { IconTrash } from "@tabler/icons-react";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
@@ -10,8 +10,7 @@ import { MantineReactTable } from "mantine-react-table";
import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
-import { useConfirmModal, useModalAction } from "@homarr/modals";
-import { InviteCreateModal } from "@homarr/modals-collection";
+import { useConfirmModal } from "@homarr/modals";
import { useScopedI18n } from "@homarr/translation/client";
import { useTranslatedMantineReactTable } from "@homarr/ui/hooks";
@@ -65,7 +64,6 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps
enableFullScreenToggle: false,
layoutMode: "grid-no-grow",
getRowId: (row) => row.id,
- renderTopToolbarCustomActions: RenderTopToolbarCustomActions,
state: {
isLoading,
},
@@ -74,22 +72,7 @@ export const InviteListComponent = ({ initialInvites }: InviteListComponentProps
},
});
- return (
- <>
- {t("title")}
-
- >
- );
-};
-
-const RenderTopToolbarCustomActions = () => {
- const t = useScopedI18n("management.page.user.invite");
- const { openModal } = useModalAction(InviteCreateModal);
- const handleNewInvite = useCallback(() => {
- openModal();
- }, [openModal]);
-
- return ;
+ return ;
};
const RenderRowActions = ({ row }: { row: MRT_Row }) => {
diff --git a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx
index f392e8282..cb5c0423c 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/invites/page.tsx
@@ -3,8 +3,11 @@ import { notFound } from "next/navigation";
import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
+import { getScopedI18n } from "@homarr/translation/server";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManageMobilePrimaryAction } from "~/components/manage/manage-mobile-primary-action";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
+import { InviteCreateButton } from "./_components/invite-create-button";
import { InviteListComponent } from "./_components/invite-list";
export default async function InvitesOverviewPage() {
@@ -17,11 +20,20 @@ export default async function InvitesOverviewPage() {
return notFound();
}
+ const t = await getScopedI18n("management.page.user.invite");
const initialInvites = await api.invite.getAll();
+
return (
- <>
-
+
+
+
+ }
+ floatingPrimaryAction
+ >
- >
+
);
}
diff --git a/apps/nextjs/src/app/[locale]/manage/users/page.tsx b/apps/nextjs/src/app/[locale]/manage/users/page.tsx
index 94ae5c103..4059b9972 100644
--- a/apps/nextjs/src/app/[locale]/manage/users/page.tsx
+++ b/apps/nextjs/src/app/[locale]/manage/users/page.tsx
@@ -4,8 +4,10 @@ import { api } from "@homarr/api/server";
import { auth } from "@homarr/auth/next";
import { isProviderEnabled } from "@homarr/auth/server";
import { getScopedI18n } from "@homarr/translation/server";
+import { Link } from "@homarr/ui";
-import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManagePageLayout } from "~/components/manage/manage-page-layout";
+import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
import { createMetaTitle } from "~/metadata";
import { UserListComponent } from "./_components/user-list";
@@ -27,13 +29,23 @@ export default async function UsersPage() {
return notFound();
}
+ const t = await getScopedI18n("management.page.user");
const userList = await api.user.getAll();
const credentialsProviderEnabled = isProviderEnabled("credentials");
return (
- <>
-
-
- >
+
+ {t("create.title")}
+
+ ) : undefined
+ }
+ floatingPrimaryAction={credentialsProviderEnabled}
+ >
+
+
);
}
diff --git a/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx b/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx
new file mode 100644
index 000000000..40192e0f8
--- /dev/null
+++ b/apps/nextjs/src/components/manage/manage-mobile-primary-action.tsx
@@ -0,0 +1,15 @@
+import type { PropsWithChildren } from "react";
+import { Affix, Box } from "@mantine/core";
+
+import { MANAGE_ACTION_BUTTON_MIN_WIDTH, MANAGE_AFFIX_POSITION } from "./manage-page.constants";
+
+export const ManageMobilePrimaryAction = ({ children }: PropsWithChildren) => (
+ <>
+
+ {children}
+
+
+ {children}
+
+ >
+);
diff --git a/apps/nextjs/src/components/manage/manage-page-layout.tsx b/apps/nextjs/src/components/manage/manage-page-layout.tsx
new file mode 100644
index 000000000..d8655c58c
--- /dev/null
+++ b/apps/nextjs/src/components/manage/manage-page-layout.tsx
@@ -0,0 +1,44 @@
+import type { ReactNode } from "react";
+import type { MantineSize } from "@mantine/core";
+import { Group, Stack, Title } from "@mantine/core";
+
+import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
+import { ManageContainer } from "./manage-container";
+import { MANAGE_FLOATING_ACTION_BOTTOM_OFFSET } from "./manage-page.constants";
+
+interface ManagePageLayoutProps {
+ title: ReactNode;
+ primaryAction?: ReactNode;
+ toolbar?: ReactNode;
+ footer?: ReactNode;
+ floatingPrimaryAction?: boolean;
+ size?: MantineSize;
+ children: ReactNode;
+}
+
+export const ManagePageLayout = ({
+ title,
+ primaryAction,
+ toolbar,
+ footer,
+ floatingPrimaryAction,
+ size,
+ children,
+}: ManagePageLayoutProps) => {
+ const titleNode = typeof title === "string" ? {title} : title;
+
+ return (
+
+
+
+
+ {titleNode}
+ {primaryAction}
+
+ {toolbar}
+ {children}
+ {footer && {footer}}
+
+
+ );
+};
diff --git a/apps/nextjs/src/components/manage/manage-page.constants.ts b/apps/nextjs/src/components/manage/manage-page.constants.ts
new file mode 100644
index 000000000..7b3a59884
--- /dev/null
+++ b/apps/nextjs/src/components/manage/manage-page.constants.ts
@@ -0,0 +1,5 @@
+export const MANAGE_AFFIX_POSITION = { bottom: 20, right: 20 } as const;
+
+export const MANAGE_FLOATING_ACTION_BOTTOM_OFFSET = 48;
+
+export const MANAGE_ACTION_BUTTON_MIN_WIDTH = 160;
diff --git a/apps/nextjs/src/components/manage/mobile-affix-button.tsx b/apps/nextjs/src/components/manage/mobile-affix-button.tsx
index abe4f017c..f711751de 100644
--- a/apps/nextjs/src/components/manage/mobile-affix-button.tsx
+++ b/apps/nextjs/src/components/manage/mobile-affix-button.tsx
@@ -2,13 +2,15 @@ import { forwardRef } from "react";
import type { ButtonProps } from "@mantine/core";
import { Affix, Button, createPolymorphicComponent } from "@mantine/core";
+import { MANAGE_ACTION_BUTTON_MIN_WIDTH, MANAGE_AFFIX_POSITION } from "./manage-page.constants";
+
type MobileAffixButtonProps = Omit;
export const MobileAffixButton = createPolymorphicComponent<"button", MobileAffixButtonProps>(
forwardRef((props, ref) => (
<>
-
-
+
+
>
diff --git a/apps/nextjs/src/components/no-results.tsx b/apps/nextjs/src/components/no-results.tsx
index db492c2e1..ecc794e46 100644
--- a/apps/nextjs/src/components/no-results.tsx
+++ b/apps/nextjs/src/components/no-results.tsx
@@ -20,7 +20,7 @@ export const NoResults = ({ icon: Icon, title, action }: NoResultsProps) => {
{title}
- {!action?.hidden && {action?.label}}
+ {action && !action.hidden && {action.label}}
);
diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json
index 29e86458e..d7b690137 100644
--- a/packages/translation/src/lang/en.json
+++ b/packages/translation/src/lang/en.json
@@ -1054,6 +1054,9 @@
"open": {
"label": "Open media"
}
+ },
+ "noResults": {
+ "title": "No media uploaded"
}
},
"common": {
@@ -3452,6 +3455,9 @@
"public": "This board is public",
"private": "This board is private"
},
+ "noResults": {
+ "title": "No boards yet"
+ },
"modal": {
"createBoard": {
"field": {