style: improve mobile compatibility of certain manage pages (#678)
* style: improve mobile compatibility of certain manage pages * style: improve mobile support for more manage pages * fix: format issues * chore: address pull request feedback
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
.bannerContainer {
|
.bannerContainer {
|
||||||
padding: 3rem;
|
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: linear-gradient(
|
background: linear-gradient(
|
||||||
|
|||||||
@@ -38,17 +38,17 @@ export const HeroBanner = () => {
|
|||||||
const gridSpan = 12 / countIconGroups;
|
const gridSpan = 12 / countIconGroups;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box className={classes.bannerContainer} bg="dark.6" pos="relative">
|
<Box className={classes.bannerContainer} p={{ base: "lg", md: "3rem" }} bg="dark.6" pos="relative">
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Title order={2} c="dimmed">
|
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
|
||||||
Welcome back to your
|
Welcome back to your
|
||||||
</Title>
|
</Title>
|
||||||
<Group gap="xs">
|
<Group gap="xs" wrap="nowrap">
|
||||||
<Image src="/logo/logo.png" w={40} h={40} />
|
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
|
||||||
<Title>Homarr Dashboard</Title>
|
<Title fz={{ base: "h3", md: "h1" }}>Homarr Board</Title>
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
<Box className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
<Box visibleFrom="md" className={classes.scrollContainer} w={"30%"} top={0} right={0} pos="absolute">
|
||||||
<Grid>
|
<Grid>
|
||||||
{Array(countIconGroups)
|
{Array(countIconGroups)
|
||||||
.fill(0)
|
.fill(0)
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
|
|||||||
}, [app, mutate, t, openConfirmModal]);
|
}, [app, mutate, t, openConfirmModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ActionIcon loading={isPending} variant="subtle" color="red" onClick={onClick} aria-label="Delete app">
|
<ActionIcon loading={isPending} variant="subtle" color="red" onClick={onClick} aria-label={t("title")}>
|
||||||
<IconTrash color="red" size={16} stroke={1.5} />
|
<IconTrash color="red" size={16} stroke={1.5} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,23 +1,13 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
ActionIcon,
|
|
||||||
ActionIconGroup,
|
|
||||||
Anchor,
|
|
||||||
Avatar,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
Container,
|
|
||||||
Group,
|
|
||||||
Stack,
|
|
||||||
Text,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
import { IconApps, IconPencil } from "@tabler/icons-react";
|
import { IconApps, IconPencil } from "@tabler/icons-react";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
import { AppDeleteButton } from "./_app-delete-button";
|
import { AppDeleteButton } from "./_app-delete-button";
|
||||||
|
|
||||||
export default async function AppsPage() {
|
export default async function AppsPage() {
|
||||||
@@ -25,13 +15,13 @@ export default async function AppsPage() {
|
|||||||
const t = await getScopedI18n("app");
|
const t = await getScopedI18n("app");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<ManageContainer>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Title>{t("page.list.title")}</Title>
|
<Title>{t("page.list.title")}</Title>
|
||||||
<Button component={Link} href="/manage/apps/new">
|
<MobileAffixButton component={Link} href="/manage/apps/new">
|
||||||
{t("page.create.title")}
|
{t("page.create.title")}
|
||||||
</Button>
|
</MobileAffixButton>
|
||||||
</Group>
|
</Group>
|
||||||
{apps.length === 0 && <AppNoResults />}
|
{apps.length === 0 && <AppNoResults />}
|
||||||
{apps.length > 0 && (
|
{apps.length > 0 && (
|
||||||
@@ -42,7 +32,7 @@ export default async function AppsPage() {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,10 +40,12 @@ interface AppCardProps {
|
|||||||
app: RouterOutputs["app"]["all"][number];
|
app: RouterOutputs["app"]["all"][number];
|
||||||
}
|
}
|
||||||
|
|
||||||
const AppCard = ({ app }: AppCardProps) => {
|
const AppCard = async ({ app }: AppCardProps) => {
|
||||||
|
const t = await getScopedI18n("app");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group align="top" justify="start" wrap="nowrap">
|
<Group align="top" justify="start" wrap="nowrap">
|
||||||
<Avatar
|
<Avatar
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -66,14 +58,16 @@ const AppCard = ({ app }: AppCardProps) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Text fw={500}>{app.name}</Text>
|
<Text fw={500} lineClamp={1}>
|
||||||
|
{app.name}
|
||||||
|
</Text>
|
||||||
{app.description && (
|
{app.description && (
|
||||||
<Text size="sm" c="gray.6">
|
<Text size="sm" c="gray.6" lineClamp={4}>
|
||||||
{app.description}
|
{app.description}
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
)}
|
||||||
{app.href && (
|
{app.href && (
|
||||||
<Anchor href={app.href} size="sm" w="min-content">
|
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
|
||||||
{app.href}
|
{app.href}
|
||||||
</Anchor>
|
</Anchor>
|
||||||
)}
|
)}
|
||||||
@@ -86,7 +80,7 @@ const AppCard = ({ app }: AppCardProps) => {
|
|||||||
href={`/manage/apps/edit/${app.id}`}
|
href={`/manage/apps/edit/${app.id}`}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
aria-label="Edit app"
|
aria-label={t("page.edit.title")}
|
||||||
>
|
>
|
||||||
<IconPencil size={16} stroke={1.5} />
|
<IconPencil size={16} stroke={1.5} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Button } from "@mantine/core";
|
|
||||||
import { IconCategoryPlus } from "@tabler/icons-react";
|
import { IconCategoryPlus } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
@@ -10,6 +9,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
|
||||||
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
|
|
||||||
interface CreateBoardButtonProps {
|
interface CreateBoardButtonProps {
|
||||||
boardNames: string[];
|
boardNames: string[];
|
||||||
@@ -37,8 +37,8 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
|
|||||||
}, [mutateAsync, boardNames, openModal]);
|
}, [mutateAsync, boardNames, openModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button leftSection={<IconCategoryPlus size="1rem" />} onClick={onClick} loading={isPending}>
|
<MobileAffixButton leftSection={<IconCategoryPlus size="1rem" />} onClick={onClick} loading={isPending}>
|
||||||
{t("management.page.board.action.new.label")}
|
{t("management.page.board.action.new.label")}
|
||||||
</Button>
|
</MobileAffixButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
MenuTarget,
|
MenuTarget,
|
||||||
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
@@ -22,6 +23,7 @@ import { getScopedI18n } from "@homarr/translation/server";
|
|||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
|
||||||
import { CreateBoardButton } from "./_components/create-board-button";
|
import { CreateBoardButton } from "./_components/create-board-button";
|
||||||
|
|
||||||
@@ -31,20 +33,22 @@ export default async function ManageBoardsPage() {
|
|||||||
const boards = await api.board.getAllBoards();
|
const boards = await api.board.getAllBoards();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<ManageContainer>
|
||||||
<Group justify="space-between">
|
<Stack>
|
||||||
<Title mb="md">{t("title")}</Title>
|
<Group justify="space-between">
|
||||||
<CreateBoardButton boardNames={boards.map((board) => board.name)} />
|
<Title mb="md">{t("title")}</Title>
|
||||||
</Group>
|
<CreateBoardButton boardNames={boards.map((board) => board.name)} />
|
||||||
|
</Group>
|
||||||
|
|
||||||
<Grid>
|
<Grid mb={{ base: "xl", md: 0 }}>
|
||||||
{boards.map((board) => (
|
{boards.map((board) => (
|
||||||
<GridCol span={{ base: 12, md: 6, xl: 4 }} key={board.id}>
|
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
|
||||||
<BoardCard board={board} />
|
<BoardCard board={board} />
|
||||||
</GridCol>
|
</GridCol>
|
||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
</>
|
</Stack>
|
||||||
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export const DeleteIntegrationActionButton = ({ count, integration }: DeleteInte
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
aria-label="Delete integration"
|
aria-label={t("title")}
|
||||||
>
|
>
|
||||||
<IconTrash color="red" size={16} stroke={1.5} />
|
<IconTrash color="red" size={16} stroke={1.5} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import type { ChangeEvent } from "react";
|
import type { ChangeEvent } from "react";
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Group, Menu, ScrollArea, Stack, Text, TextInput } from "@mantine/core";
|
import { Flex, Group, Menu, ScrollArea, Text, TextInput } from "@mantine/core";
|
||||||
import { IconSearch } from "@tabler/icons-react";
|
import { IconSearch } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
||||||
@@ -25,7 +25,7 @@ export const IntegrationCreateDropdownContent = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack>
|
<Flex direction={{ base: "column-reverse", md: "column" }} gap="sm">
|
||||||
<TextInput
|
<TextInput
|
||||||
leftSection={<IconSearch stroke={1.5} size={20} />}
|
leftSection={<IconSearch stroke={1.5} size={20} />}
|
||||||
placeholder={t("integration.page.list.search")}
|
placeholder={t("integration.page.list.search")}
|
||||||
@@ -47,6 +47,6 @@ export const IntegrationCreateDropdownContent = () => {
|
|||||||
) : (
|
) : (
|
||||||
<Menu.Item disabled>{t("common.noResults")}</Menu.Item>
|
<Menu.Item disabled>{t("common.noResults")}</Menu.Item>
|
||||||
)}
|
)}
|
||||||
</Stack>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { Fragment } from "react";
|
||||||
|
import type { PropsWithChildren } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import {
|
||||||
AccordionControl,
|
AccordionControl,
|
||||||
@@ -5,9 +7,11 @@ import {
|
|||||||
AccordionPanel,
|
AccordionPanel,
|
||||||
ActionIcon,
|
ActionIcon,
|
||||||
ActionIconGroup,
|
ActionIconGroup,
|
||||||
|
Affix,
|
||||||
Anchor,
|
Anchor,
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
Container,
|
Divider,
|
||||||
Group,
|
Group,
|
||||||
Menu,
|
Menu,
|
||||||
MenuDropdown,
|
MenuDropdown,
|
||||||
@@ -22,7 +26,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
Title,
|
Title,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { IconChevronDown, IconPencil } from "@tabler/icons-react";
|
import { IconChevronDown, IconChevronUp, IconPencil } from "@tabler/icons-react";
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -32,6 +36,7 @@ import { getIntegrationName } from "@homarr/definitions";
|
|||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
import { CountBadge } from "@homarr/ui";
|
import { CountBadge } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
||||||
import { IntegrationAvatar } from "./_integration-avatar";
|
import { IntegrationAvatar } from "./_integration-avatar";
|
||||||
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
||||||
@@ -48,26 +53,47 @@ export default async function IntegrationsPage({ searchParams }: IntegrationsPag
|
|||||||
const t = await getScopedI18n("integration");
|
const t = await getScopedI18n("integration");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<ManageContainer>
|
||||||
<Stack>
|
<Stack>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
<Title>{t("page.list.title")}</Title>
|
<Title>{t("page.list.title")}</Title>
|
||||||
<Menu width={256} trapFocus position="bottom-start" withinPortal shadow="md" keepMounted={false}>
|
|
||||||
<MenuTarget>
|
<Box>
|
||||||
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
<IntegrationSelectMenu>
|
||||||
</MenuTarget>
|
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
|
||||||
<MenuDropdown>
|
<MenuTarget>
|
||||||
<IntegrationCreateDropdownContent />
|
<Button rightSection={<IconChevronUp size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
||||||
</MenuDropdown>
|
</MenuTarget>
|
||||||
</Menu>
|
</Affix>
|
||||||
|
</IntegrationSelectMenu>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box visibleFrom="md">
|
||||||
|
<IntegrationSelectMenu>
|
||||||
|
<MenuTarget>
|
||||||
|
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
|
||||||
|
</MenuTarget>
|
||||||
|
</IntegrationSelectMenu>
|
||||||
|
</Box>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
|
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
||||||
|
return (
|
||||||
|
<Menu width={256} trapFocus position="bottom-end" withinPortal shadow="md" keepMounted={false}>
|
||||||
|
{children}
|
||||||
|
<MenuDropdown>
|
||||||
|
<IntegrationCreateDropdownContent />
|
||||||
|
</MenuDropdown>
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IntegrationListProps {
|
interface IntegrationListProps {
|
||||||
integrations: RouterOutputs["integration"]["all"];
|
integrations: RouterOutputs["integration"]["all"];
|
||||||
activeTab?: IntegrationKind;
|
activeTab?: IntegrationKind;
|
||||||
@@ -105,7 +131,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
|
|||||||
</Group>
|
</Group>
|
||||||
</AccordionControl>
|
</AccordionControl>
|
||||||
<AccordionPanel>
|
<AccordionPanel>
|
||||||
<Table>
|
<Table visibleFrom="md">
|
||||||
<TableThead>
|
<TableThead>
|
||||||
<TableTr>
|
<TableTr>
|
||||||
<TableTh>{t("field.name.label")}</TableTh>
|
<TableTh>{t("field.name.label")}</TableTh>
|
||||||
@@ -130,7 +156,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
|
|||||||
href={`/manage/integrations/edit/${integration.id}`}
|
href={`/manage/integrations/edit/${integration.id}`}
|
||||||
variant="subtle"
|
variant="subtle"
|
||||||
color="gray"
|
color="gray"
|
||||||
aria-label="Edit integration"
|
aria-label={t("page.edit.title", { name: getIntegrationName(integration.kind) })}
|
||||||
>
|
>
|
||||||
<IconPencil size={16} stroke={1.5} />
|
<IconPencil size={16} stroke={1.5} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
@@ -142,6 +168,34 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
|
|||||||
))}
|
))}
|
||||||
</TableTbody>
|
</TableTbody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
|
<Stack gap="xs" hiddenFrom="md">
|
||||||
|
{integrations.map((integration, index) => (
|
||||||
|
<Fragment key={integration.id}>
|
||||||
|
{index !== 0 && <Divider />}
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
|
<Text>{integration.name}</Text>
|
||||||
|
<ActionIconGroup>
|
||||||
|
<ActionIcon
|
||||||
|
component={Link}
|
||||||
|
href={`/manage/integrations/edit/${integration.id}`}
|
||||||
|
variant="subtle"
|
||||||
|
color="gray"
|
||||||
|
aria-label={t("page.edit.title", { name: getIntegrationName(integration.kind) })}
|
||||||
|
>
|
||||||
|
<IconPencil size={16} stroke={1.5} />
|
||||||
|
</ActionIcon>
|
||||||
|
<DeleteIntegrationActionButton integration={integration} count={integrations.length} />
|
||||||
|
</ActionIconGroup>
|
||||||
|
</Group>
|
||||||
|
<Anchor href={integration.url} target="_blank" rel="noreferrer" size="sm">
|
||||||
|
{integration.url}
|
||||||
|
</Anchor>
|
||||||
|
</Stack>
|
||||||
|
</Fragment>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
</AccordionPanel>
|
</AccordionPanel>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ export default async function ManagementPage() {
|
|||||||
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||||
{links.map((link, index) => (
|
{links.map((link, index) => (
|
||||||
<Card component={Link} href={link.href} key={`link-${index}`} withBorder>
|
<Card component={Link} href={link.href} key={`link-${index}`} withBorder>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between" wrap="nowrap">
|
||||||
<Group>
|
<Group wrap="nowrap">
|
||||||
<Text size="2.4rem" fw="bolder">
|
<Text size="2.4rem" fw="bolder">
|
||||||
{link.count}
|
{link.count}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
@@ -109,7 +109,9 @@ const SwitchSetting = ({
|
|||||||
<UnstyledButton style={{ flexGrow: 1 }} onClick={handleClick}>
|
<UnstyledButton style={{ flexGrow: 1 }} onClick={handleClick}>
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Text fw="bold">{title}</Text>
|
<Text fw="bold">{title}</Text>
|
||||||
<Text c="gray.5">{text}</Text>
|
<Text c="gray.5" fz={{ base: "xs", md: "sm" }}>
|
||||||
|
{text}
|
||||||
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</UnstyledButton>
|
</UnstyledButton>
|
||||||
<Switch disabled={disabled} onClick={handleClick} checked={form.values[formKey] && !disabled} />
|
<Switch disabled={disabled} onClick={handleClick} checked={form.values[formKey] && !disabled} />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { notFound } from "next/navigation";
|
import { notFound } from "next/navigation";
|
||||||
import { Button, Container, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
import { Button, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconSettings, IconShieldLock } from "@tabler/icons-react";
|
import { IconSettings, IconShieldLock } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -9,6 +9,7 @@ import { auth } from "@homarr/auth/next";
|
|||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
import { UserAvatar } from "@homarr/ui";
|
import { UserAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
|
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
|
||||||
import { NavigationLink } from "../groups/[id]/_navigation";
|
import { NavigationLink } from "../groups/[id]/_navigation";
|
||||||
import { canAccessUserEditPage } from "./access";
|
import { canAccessUserEditPage } from "./access";
|
||||||
@@ -28,7 +29,7 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl">
|
<ManageContainer size="xl">
|
||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
@@ -64,6 +65,6 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
|
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import type { PropsWithChildren } from "react";
|
import type { PropsWithChildren } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button, Container, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
import { Button, Grid, GridCol, Group, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react";
|
import { IconLock, IconSettings, IconUsersGroup } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
import { getI18n, getScopedI18n } from "@homarr/translation/server";
|
||||||
|
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { NavigationLink } from "./_navigation";
|
import { NavigationLink } from "./_navigation";
|
||||||
|
|
||||||
interface LayoutProps {
|
interface LayoutProps {
|
||||||
@@ -18,7 +19,7 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
|
|||||||
const group = await api.group.getById({ id: params.id });
|
const group = await api.group.getById({ id: params.id });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl">
|
<ManageContainer size="xl">
|
||||||
<Grid>
|
<Grid>
|
||||||
<GridCol span={12}>
|
<GridCol span={12}>
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center">
|
||||||
@@ -54,6 +55,6 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
|
|||||||
</GridCol>
|
</GridCol>
|
||||||
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
|
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Container>
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback } from "react";
|
import { useCallback } from "react";
|
||||||
import { Button } from "@mantine/core";
|
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
import { useModalAction } from "@homarr/modals";
|
import { useModalAction } from "@homarr/modals";
|
||||||
@@ -9,6 +8,7 @@ import { useScopedI18n } from "@homarr/translation/client";
|
|||||||
|
|
||||||
import { UserSelectModal } from "~/app/[locale]/boards/[name]/settings/_access/user-select-modal";
|
import { UserSelectModal } from "~/app/[locale]/boards/[name]/settings/_access/user-select-modal";
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
|
|
||||||
interface AddGroupMemberProps {
|
interface AddGroupMemberProps {
|
||||||
groupId: string;
|
groupId: string;
|
||||||
@@ -40,8 +40,8 @@ export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps)
|
|||||||
}, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]);
|
}, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button color="teal" onClick={handleAddMember}>
|
<MobileAffixButton color="teal" onClick={handleAddMember}>
|
||||||
{tMembersAdd("label")}
|
{tMembersAdd("label")}
|
||||||
</Button>
|
</MobileAffixButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ interface PermissionRowProps {
|
|||||||
|
|
||||||
const PermissionRow = ({ name, label, description }: PermissionRowProps) => {
|
const PermissionRow = ({ name, label, description }: PermissionRowProps) => {
|
||||||
return (
|
return (
|
||||||
<Group justify="space-between" align="center">
|
<Group justify="space-between" align="center" wrap="nowrap">
|
||||||
<Stack gap={0}>
|
<Stack gap={0}>
|
||||||
<Text fw={500}>{label}</Text>
|
<Text fw={500}>{label}</Text>
|
||||||
<Text c="gray.5">{description}</Text>
|
<Text c="gray.5">{description}</Text>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
|
|||||||
import { validation } from "@homarr/validation";
|
import { validation } from "@homarr/validation";
|
||||||
|
|
||||||
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
|
||||||
|
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
|
||||||
|
|
||||||
export const AddGroup = () => {
|
export const AddGroup = () => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
@@ -21,9 +22,9 @@ export const AddGroup = () => {
|
|||||||
}, [openModal]);
|
}, [openModal]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button onClick={handleAddGroup} color="teal">
|
<MobileAffixButton onClick={handleAddGroup} color="teal">
|
||||||
{t("group.action.create.label")}
|
{t("group.action.create.label")}
|
||||||
</Button>
|
</MobileAffixButton>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,5 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import {
|
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
|
||||||
Anchor,
|
|
||||||
Container,
|
|
||||||
Group,
|
|
||||||
Stack,
|
|
||||||
Table,
|
|
||||||
TableTbody,
|
|
||||||
TableTd,
|
|
||||||
TableTh,
|
|
||||||
TableThead,
|
|
||||||
TableTr,
|
|
||||||
Title,
|
|
||||||
} from "@mantine/core";
|
|
||||||
|
|
||||||
import type { RouterOutputs } from "@homarr/api";
|
import type { RouterOutputs } from "@homarr/api";
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
@@ -19,6 +7,7 @@ import { getI18n } from "@homarr/translation/server";
|
|||||||
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
|
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
|
||||||
import { z } from "@homarr/validation";
|
import { z } from "@homarr/validation";
|
||||||
|
|
||||||
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { AddGroup } from "./_add-group";
|
import { AddGroup } from "./_add-group";
|
||||||
|
|
||||||
const searchParamsSchema = z.object({
|
const searchParamsSchema = z.object({
|
||||||
@@ -41,7 +30,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
|
|||||||
const { items: groups, totalCount } = await api.group.getPaginated(searchParams);
|
const { items: groups, totalCount } = await api.group.getPaginated(searchParams);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container size="xl">
|
<ManageContainer size="xl">
|
||||||
<Stack>
|
<Stack>
|
||||||
<Title>{t("group.title")}</Title>
|
<Title>{t("group.title")}</Title>
|
||||||
<Group justify="space-between">
|
<Group justify="space-between">
|
||||||
@@ -72,7 +61,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
|
|||||||
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
|
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
|
||||||
</Group>
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Container>
|
</ManageContainer>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
11
apps/nextjs/src/components/manage/manage-container.tsx
Normal file
11
apps/nextjs/src/components/manage/manage-container.tsx
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import type { PropsWithChildren } from "react";
|
||||||
|
import type { MantineSize } from "@mantine/core";
|
||||||
|
import { Container } from "@mantine/core";
|
||||||
|
|
||||||
|
export const ManageContainer = ({ children, size }: PropsWithChildren<{ size?: MantineSize }>) => {
|
||||||
|
return (
|
||||||
|
<Container size={size} px={{ base: "0 !important", md: "var(--mantine-spacing-md) !important" }}>
|
||||||
|
{children}
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
16
apps/nextjs/src/components/manage/mobile-affix-button.tsx
Normal file
16
apps/nextjs/src/components/manage/mobile-affix-button.tsx
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { forwardRef } from "react";
|
||||||
|
import type { ButtonProps } from "@mantine/core";
|
||||||
|
import { Affix, Button, createPolymorphicComponent } from "@mantine/core";
|
||||||
|
|
||||||
|
type MobileAffixButtonProps = Omit<ButtonProps, "visibleFrom" | "hiddenFrom">;
|
||||||
|
|
||||||
|
export const MobileAffixButton = createPolymorphicComponent<"button", MobileAffixButtonProps>(
|
||||||
|
forwardRef<HTMLButtonElement, MobileAffixButtonProps>((props, ref) => (
|
||||||
|
<>
|
||||||
|
<Button ref={ref} visibleFrom="md" {...props} />
|
||||||
|
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
|
||||||
|
<Button ref={ref} {...props} />
|
||||||
|
</Affix>
|
||||||
|
</>
|
||||||
|
)),
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user