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:
Meier Lukas
2024-06-22 21:02:17 +02:00
committed by GitHub
parent f92aeba403
commit 6029a5b592
19 changed files with 167 additions and 95 deletions

View File

@@ -1,5 +1,4 @@
.bannerContainer {
padding: 3rem;
border-radius: 8px;
overflow: hidden;
background: linear-gradient(

View File

@@ -38,17 +38,17 @@ export const HeroBanner = () => {
const gridSpan = 12 / countIconGroups;
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}>
<Title order={2} c="dimmed">
<Title fz={{ base: "h4", md: "h2" }} c="dimmed">
Welcome back to your
</Title>
<Group gap="xs">
<Image src="/logo/logo.png" w={40} h={40} />
<Title>Homarr Dashboard</Title>
<Group gap="xs" wrap="nowrap">
<Image src="/logo/logo.png" w={{ base: 32, md: 40 }} h={{ base: 32, md: 40 }} />
<Title fz={{ base: "h3", md: "h1" }}>Homarr Board</Title>
</Group>
</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>
{Array(countIconGroups)
.fill(0)

View File

@@ -49,7 +49,7 @@ export const AppDeleteButton = ({ app }: AppDeleteButtonProps) => {
}, [app, mutate, t, openConfirmModal]);
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} />
</ActionIcon>
);

View File

@@ -1,23 +1,13 @@
import Link from "next/link";
import {
ActionIcon,
ActionIconGroup,
Anchor,
Avatar,
Button,
Card,
Container,
Group,
Stack,
Text,
Title,
} from "@mantine/core";
import { ActionIcon, ActionIconGroup, Anchor, Avatar, Card, Group, Stack, Text, Title } from "@mantine/core";
import { IconApps, IconPencil } from "@tabler/icons-react";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/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";
export default async function AppsPage() {
@@ -25,13 +15,13 @@ export default async function AppsPage() {
const t = await getScopedI18n("app");
return (
<Container>
<ManageContainer>
<Stack>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
<Button component={Link} href="/manage/apps/new">
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
</Button>
</MobileAffixButton>
</Group>
{apps.length === 0 && <AppNoResults />}
{apps.length > 0 && (
@@ -42,7 +32,7 @@ export default async function AppsPage() {
</Stack>
)}
</Stack>
</Container>
</ManageContainer>
);
}
@@ -50,10 +40,12 @@ interface AppCardProps {
app: RouterOutputs["app"]["all"][number];
}
const AppCard = ({ app }: AppCardProps) => {
const AppCard = async ({ app }: AppCardProps) => {
const t = await getScopedI18n("app");
return (
<Card>
<Group justify="space-between">
<Group justify="space-between" wrap="nowrap">
<Group align="top" justify="start" wrap="nowrap">
<Avatar
size="sm"
@@ -66,14 +58,16 @@ const AppCard = ({ app }: AppCardProps) => {
}}
/>
<Stack gap={0}>
<Text fw={500}>{app.name}</Text>
<Text fw={500} lineClamp={1}>
{app.name}
</Text>
{app.description && (
<Text size="sm" c="gray.6">
<Text size="sm" c="gray.6" lineClamp={4}>
{app.description}
</Text>
)}
{app.href && (
<Anchor href={app.href} size="sm" w="min-content">
<Anchor href={app.href} lineClamp={1} size="sm" w="min-content">
{app.href}
</Anchor>
)}
@@ -86,7 +80,7 @@ const AppCard = ({ app }: AppCardProps) => {
href={`/manage/apps/edit/${app.id}`}
variant="subtle"
color="gray"
aria-label="Edit app"
aria-label={t("page.edit.title")}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>

View File

@@ -1,7 +1,6 @@
"use client";
import { useCallback } from "react";
import { Button } from "@mantine/core";
import { IconCategoryPlus } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
@@ -10,6 +9,7 @@ import { useI18n } from "@homarr/translation/client";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { AddBoardModal } from "~/components/manage/boards/add-board-modal";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
interface CreateBoardButtonProps {
boardNames: string[];
@@ -37,8 +37,8 @@ export const CreateBoardButton = ({ boardNames }: CreateBoardButtonProps) => {
}, [mutateAsync, boardNames, openModal]);
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")}
</Button>
</MobileAffixButton>
);
};

View File

@@ -10,6 +10,7 @@ import {
Group,
Menu,
MenuTarget,
Stack,
Text,
Title,
Tooltip,
@@ -22,6 +23,7 @@ import { getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui";
import { getBoardPermissionsAsync } from "~/components/board/permissions/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { BoardCardMenuDropdown } from "./_components/board-card-menu-dropdown";
import { CreateBoardButton } from "./_components/create-board-button";
@@ -31,20 +33,22 @@ export default async function ManageBoardsPage() {
const boards = await api.board.getAllBoards();
return (
<>
<Group justify="space-between">
<Title mb="md">{t("title")}</Title>
<CreateBoardButton boardNames={boards.map((board) => board.name)} />
</Group>
<ManageContainer>
<Stack>
<Group justify="space-between">
<Title mb="md">{t("title")}</Title>
<CreateBoardButton boardNames={boards.map((board) => board.name)} />
</Group>
<Grid>
{boards.map((board) => (
<GridCol span={{ base: 12, md: 6, xl: 4 }} key={board.id}>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</>
<Grid mb={{ base: "xl", md: 0 }}>
{boards.map((board) => (
<GridCol span={{ base: 12, md: 6 }} key={board.id}>
<BoardCard board={board} />
</GridCol>
))}
</Grid>
</Stack>
</ManageContainer>
);
}

View File

@@ -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} />
</ActionIcon>

View File

@@ -3,7 +3,7 @@
import type { ChangeEvent } from "react";
import React, { useMemo, useState } from "react";
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 { getIntegrationName, integrationKinds } from "@homarr/definitions";
@@ -25,7 +25,7 @@ export const IntegrationCreateDropdownContent = () => {
);
return (
<Stack>
<Flex direction={{ base: "column-reverse", md: "column" }} gap="sm">
<TextInput
leftSection={<IconSearch stroke={1.5} size={20} />}
placeholder={t("integration.page.list.search")}
@@ -47,6 +47,6 @@ export const IntegrationCreateDropdownContent = () => {
) : (
<Menu.Item disabled>{t("common.noResults")}</Menu.Item>
)}
</Stack>
</Flex>
);
};

View File

@@ -1,3 +1,5 @@
import { Fragment } from "react";
import type { PropsWithChildren } from "react";
import Link from "next/link";
import {
AccordionControl,
@@ -5,9 +7,11 @@ import {
AccordionPanel,
ActionIcon,
ActionIconGroup,
Affix,
Anchor,
Box,
Button,
Container,
Divider,
Group,
Menu,
MenuDropdown,
@@ -22,7 +26,7 @@ import {
Text,
Title,
} 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 { api } from "@homarr/api/server";
@@ -32,6 +36,7 @@ import { getIntegrationName } from "@homarr/definitions";
import { getScopedI18n } from "@homarr/translation/server";
import { CountBadge } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { IntegrationAvatar } from "./_integration-avatar";
import { DeleteIntegrationActionButton } from "./_integration-buttons";
@@ -48,26 +53,47 @@ export default async function IntegrationsPage({ searchParams }: IntegrationsPag
const t = await getScopedI18n("integration");
return (
<Container>
<ManageContainer>
<Stack>
<Group justify="space-between" align="center">
<Title>{t("page.list.title")}</Title>
<Menu width={256} trapFocus position="bottom-start" withinPortal shadow="md" keepMounted={false}>
<MenuTarget>
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
<MenuDropdown>
<IntegrationCreateDropdownContent />
</MenuDropdown>
</Menu>
<Box>
<IntegrationSelectMenu>
<Affix hiddenFrom="md" position={{ bottom: 20, right: 20 }}>
<MenuTarget>
<Button rightSection={<IconChevronUp size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
</Affix>
</IntegrationSelectMenu>
</Box>
<Box visibleFrom="md">
<IntegrationSelectMenu>
<MenuTarget>
<Button rightSection={<IconChevronDown size={16} stroke={1.5} />}>{t("action.create")}</Button>
</MenuTarget>
</IntegrationSelectMenu>
</Box>
</Group>
<IntegrationList integrations={integrations} activeTab={searchParams.tab} />
</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 {
integrations: RouterOutputs["integration"]["all"];
activeTab?: IntegrationKind;
@@ -105,7 +131,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
</Group>
</AccordionControl>
<AccordionPanel>
<Table>
<Table visibleFrom="md">
<TableThead>
<TableTr>
<TableTh>{t("field.name.label")}</TableTh>
@@ -130,7 +156,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
href={`/manage/integrations/edit/${integration.id}`}
variant="subtle"
color="gray"
aria-label="Edit integration"
aria-label={t("page.edit.title", { name: getIntegrationName(integration.kind) })}
>
<IconPencil size={16} stroke={1.5} />
</ActionIcon>
@@ -142,6 +168,34 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
))}
</TableTbody>
</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>
</AccordionItem>
))}

View File

@@ -72,8 +72,8 @@ export default async function ManagementPage() {
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
{links.map((link, index) => (
<Card component={Link} href={link.href} key={`link-${index}`} withBorder>
<Group justify="space-between">
<Group>
<Group justify="space-between" wrap="nowrap">
<Group wrap="nowrap">
<Text size="2.4rem" fw="bolder">
{link.count}
</Text>

View File

@@ -109,7 +109,9 @@ const SwitchSetting = ({
<UnstyledButton style={{ flexGrow: 1 }} onClick={handleClick}>
<Stack gap={0}>
<Text fw="bold">{title}</Text>
<Text c="gray.5">{text}</Text>
<Text c="gray.5" fz={{ base: "xs", md: "sm" }}>
{text}
</Text>
</Stack>
</UnstyledButton>
<Switch disabled={disabled} onClick={handleClick} checked={form.values[formKey] && !disabled} />

View File

@@ -1,7 +1,7 @@
import type { PropsWithChildren } from "react";
import Link from "next/link";
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 { api } from "@homarr/api/server";
@@ -9,6 +9,7 @@ import { auth } from "@homarr/auth/next";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { UserAvatar } from "@homarr/ui";
import { ManageContainer } from "~/components/manage/manage-container";
import { catchTrpcNotFound } from "~/errors/trpc-not-found";
import { NavigationLink } from "../groups/[id]/_navigation";
import { canAccessUserEditPage } from "./access";
@@ -28,7 +29,7 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
}
return (
<Container size="xl">
<ManageContainer size="xl">
<Grid>
<GridCol span={12}>
<Group justify="space-between" align="center">
@@ -64,6 +65,6 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
</GridCol>
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
</Grid>
</Container>
</ManageContainer>
);
}

View File

@@ -1,11 +1,12 @@
import type { PropsWithChildren } from "react";
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 { api } from "@homarr/api/server";
import { getI18n, getScopedI18n } from "@homarr/translation/server";
import { ManageContainer } from "~/components/manage/manage-container";
import { NavigationLink } from "./_navigation";
interface LayoutProps {
@@ -18,7 +19,7 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
const group = await api.group.getById({ id: params.id });
return (
<Container size="xl">
<ManageContainer size="xl">
<Grid>
<GridCol span={12}>
<Group justify="space-between" align="center">
@@ -54,6 +55,6 @@ export default async function Layout({ children, params }: PropsWithChildren<Lay
</GridCol>
<GridCol span={{ xs: 12, md: 8, lg: 9, xl: 10 }}>{children}</GridCol>
</Grid>
</Container>
</ManageContainer>
);
}

View File

@@ -1,7 +1,6 @@
"use client";
import { useCallback } from "react";
import { Button } from "@mantine/core";
import { clientApi } from "@homarr/api/client";
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 { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
interface AddGroupMemberProps {
groupId: string;
@@ -40,8 +40,8 @@ export const AddGroupMember = ({ groupId, presentUserIds }: AddGroupMemberProps)
}, [openModal, presentUserIds, groupId, mutateAsync, tMembersAdd]);
return (
<Button color="teal" onClick={handleAddMember}>
<MobileAffixButton color="teal" onClick={handleAddMember}>
{tMembersAdd("label")}
</Button>
</MobileAffixButton>
);
};

View File

@@ -101,7 +101,7 @@ interface PermissionRowProps {
const PermissionRow = ({ name, label, description }: PermissionRowProps) => {
return (
<Group justify="space-between" align="center">
<Group justify="space-between" align="center" wrap="nowrap">
<Stack gap={0}>
<Text fw={500}>{label}</Text>
<Text c="gray.5">{description}</Text>

View File

@@ -11,6 +11,7 @@ import { useI18n } from "@homarr/translation/client";
import { validation } from "@homarr/validation";
import { revalidatePathActionAsync } from "~/app/revalidatePathAction";
import { MobileAffixButton } from "~/components/manage/mobile-affix-button";
export const AddGroup = () => {
const t = useI18n();
@@ -21,9 +22,9 @@ export const AddGroup = () => {
}, [openModal]);
return (
<Button onClick={handleAddGroup} color="teal">
<MobileAffixButton onClick={handleAddGroup} color="teal">
{t("group.action.create.label")}
</Button>
</MobileAffixButton>
);
};

View File

@@ -1,17 +1,5 @@
import Link from "next/link";
import {
Anchor,
Container,
Group,
Stack,
Table,
TableTbody,
TableTd,
TableTh,
TableThead,
TableTr,
Title,
} from "@mantine/core";
import { Anchor, Group, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Title } from "@mantine/core";
import type { RouterOutputs } from "@homarr/api";
import { api } from "@homarr/api/server";
@@ -19,6 +7,7 @@ import { getI18n } from "@homarr/translation/server";
import { SearchInput, TablePagination, UserAvatarGroup } from "@homarr/ui";
import { z } from "@homarr/validation";
import { ManageContainer } from "~/components/manage/manage-container";
import { AddGroup } from "./_add-group";
const searchParamsSchema = z.object({
@@ -41,7 +30,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
const { items: groups, totalCount } = await api.group.getPaginated(searchParams);
return (
<Container size="xl">
<ManageContainer size="xl">
<Stack>
<Title>{t("group.title")}</Title>
<Group justify="space-between">
@@ -72,7 +61,7 @@ export default async function GroupsListPage(props: GroupsListPageProps) {
<TablePagination total={Math.ceil(totalCount / searchParams.pageSize)} />
</Group>
</Stack>
</Container>
</ManageContainer>
);
}

View 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>
);
};

View 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>
</>
)),
);