Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/improve consistency and design #1867

Draft
wants to merge 8 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
.bannerContainer {
border-radius: 8px;
border-radius: 16px;
overflow: hidden;
@mixin dark {
background: linear-gradient(
Expand Down
35 changes: 26 additions & 9 deletions apps/nextjs/src/app/[locale]/manage/apps/_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import Link from "next/link";
import { Button, Group, Stack, Textarea, TextInput } from "@mantine/core";

import { useZodForm } from "@homarr/form";
import type { TranslationFunction } from "@homarr/translation";
import { useI18n } from "@homarr/translation/client";
import type { z } from "@homarr/validation";
import { validation } from "@homarr/validation";
Expand All @@ -14,14 +13,21 @@ import { IconPicker } from "~/components/icons/picker/icon-picker";
type FormType = z.infer<typeof validation.app.manage>;

interface AppFormProps {
submitButtonTranslation: (t: TranslationFunction) => string;
buttonLabels: {
submit: string;
submitAndCreateAnother?: string;
};
initialValues?: FormType;
handleSubmit: (values: FormType) => void;
handleSubmit: (values: FormType, redirect: boolean, afterSuccess?: () => void) => void;
isPending: boolean;
}

export const AppForm = (props: AppFormProps) => {
const { submitButtonTranslation, handleSubmit, initialValues, isPending } = props;
export const AppForm = ({
buttonLabels,
handleSubmit: originalHandleSubmit,
initialValues,
isPending,
}: AppFormProps) => {
const t = useI18n();

const form = useZodForm(validation.app.manage, {
Expand All @@ -33,20 +39,31 @@ export const AppForm = (props: AppFormProps) => {
},
});

const handleSubmitAndCreateAnother = () => {
originalHandleSubmit(form.values, false, () => {
form.reset();
});
};

return (
<form onSubmit={form.onSubmit(handleSubmit)}>
<form onSubmit={form.onSubmit((values) => originalHandleSubmit(values, true))}>
<Stack>
<TextInput {...form.getInputProps("name")} withAsterisk label={t("app.field.name.label")} />
<IconPicker initialValue={initialValues?.iconUrl} {...form.getInputProps("iconUrl")} />
<IconPicker {...form.getInputProps("iconUrl")} />
<Textarea {...form.getInputProps("description")} label={t("app.field.description.label")} />
<TextInput {...form.getInputProps("href")} label={t("app.field.url.label")} />

<Group justify="end">
<Button variant="default" component={Link} href="/manage/apps">
{t("common.action.backToOverview")}
</Button>
<Button type="submit" loading={isPending}>
{submitButtonTranslation(t)}
{buttonLabels.submitAndCreateAnother && (
<Button disabled={!form.isValid()} onClick={handleSubmitAndCreateAnother} loading={isPending}>
{buttonLabels.submitAndCreateAnother}
</Button>
)}
<Button disabled={!form.isValid()} type="submit" loading={isPending}>
{buttonLabels.submit}
</Button>
</Group>
</Stack>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ import type { RouterOutputs } from "@homarr/api";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import { useI18n, useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";

import { AppForm } from "../../_form";
Expand All @@ -18,23 +17,24 @@ interface AppEditFormProps {
}

export const AppEditForm = ({ app }: AppEditFormProps) => {
const t = useScopedI18n("app.page.edit.notification");
const tScoped = useScopedI18n("app.page.edit.notification");
const t = useI18n();
const router = useRouter();

const { mutate, isPending } = clientApi.app.update.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("success.title"),
message: t("success.message"),
title: tScoped("success.title"),
message: tScoped("success.message"),
});
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
onError: () => {
showErrorNotification({
title: t("error.title"),
message: t("error.message"),
title: tScoped("error.title"),
message: tScoped("error.message"),
});
},
});
Expand All @@ -49,11 +49,11 @@ export const AppEditForm = ({ app }: AppEditFormProps) => {
[mutate, app.id],
);

const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.save"), []);

return (
<AppForm
submitButtonTranslation={submitButtonTranslation}
buttonLabels={{
submit: t("common.action.save"),
}}
initialValues={app}
handleSubmit={handleSubmit}
isPending={isPending}
Expand Down
49 changes: 30 additions & 19 deletions apps/nextjs/src/app/[locale]/manage/apps/new/_app-new-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,55 @@ import { useRouter } from "next/navigation";
import { clientApi } from "@homarr/api/client";
import { revalidatePathActionAsync } from "@homarr/common/client";
import { showErrorNotification, showSuccessNotification } from "@homarr/notifications";
import type { TranslationFunction } from "@homarr/translation";
import { useScopedI18n } from "@homarr/translation/client";
import type { validation, z } from "@homarr/validation";

import { AppForm } from "../_form";

export const AppNewForm = () => {
const t = useScopedI18n("app.page.create.notification");
const tScoped = useScopedI18n("app.page.create.notification");
const t = useScopedI18n();
const router = useRouter();

const { mutate, isPending } = clientApi.app.create.useMutation({
onSuccess: () => {
showSuccessNotification({
title: t("success.title"),
message: t("success.message"),
});
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
onError: () => {
showErrorNotification({
title: t("error.title"),
message: t("error.message"),
title: tScoped("error.title"),
message: tScoped("error.message"),
});
},
});

const handleSubmit = useCallback(
(values: z.infer<typeof validation.app.manage>) => {
mutate(values);
(values: z.infer<typeof validation.app.manage>, redirect: boolean, afterSuccess?: () => void) => {
mutate(values, {
onSuccess() {
showSuccessNotification({
title: tScoped("success.title"),
message: tScoped("success.message"),
});
afterSuccess?.();

if (!redirect) {
return;
}
void revalidatePathActionAsync("/manage/apps").then(() => {
router.push("/manage/apps");
});
},
});
},
[mutate],
[mutate, router, tScoped],
);

const submitButtonTranslation = useCallback((t: TranslationFunction) => t("common.action.create"), []);

return (
<AppForm submitButtonTranslation={submitButtonTranslation} handleSubmit={handleSubmit} isPending={isPending} />
<AppForm
buttonLabels={{
submit: t("common.action.create"),
submitAndCreateAnother: t("common.action.createAnother"),
}}
handleSubmit={handleSubmit}
isPending={isPending}
/>
);
};
2 changes: 1 addition & 1 deletion apps/nextjs/src/app/[locale]/manage/apps/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export default async function AppsPage(props: AppsPageProps) {
<Stack>
<Title>{t("page.list.title")}</Title>
<Group justify="space-between" align="center">
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} />
<SearchInput placeholder={`${t("search")}...`} defaultValue={searchParams.search} flexExpand />
{session.user.permissions.includes("app-create") && (
<MobileAffixButton component={Link} href="/manage/apps/new">
{t("page.create.title")}
Expand Down
20 changes: 15 additions & 5 deletions apps/nextjs/src/app/[locale]/manage/boards/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ const BoardCard = async ({ board }: BoardCardProps) => {
const VisibilityIcon = board.isPublic ? IconWorld : IconLock;

return (
<Card withBorder>
<Card radius="lg" withBorder>
<CardSection p="sm" withBorder>
<Group justify="space-between" align="center">
<Group gap="sm">
Expand Down Expand Up @@ -98,15 +98,25 @@ const BoardCard = async ({ board }: BoardCardProps) => {
</Group>
</CardSection>

<CardSection p="sm">
<Group wrap="nowrap">
<Button component={Link} href={`/boards/${board.name}`} variant="default" fullWidth>
<CardSection>
<Group gap={0} wrap="nowrap">
<Button
style={{ border: "none", borderRadius: 0 }}
component={Link}
href={`/boards/${board.name}`}
variant="default"
fullWidth
>
{t("action.open.label")}
</Button>
{isMenuVisible && (
<Menu position="bottom-end">
<MenuTarget>
<ActionIcon variant="default" size="lg">
<ActionIcon
style={{ borderTop: "none", borderBottom: "none", borderRight: "none", borderRadius: 0 }}
variant="default"
size="lg"
>
<IconDotsVertical size={16} stroke={1.5} />
</ActionIcon>
</MenuTarget>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export const IntegrationCreateDropdownContent = () => {
placeholder={t("integration.page.list.search")}
value={search}
onChange={handleSearch}
variant="filled"
/>

{filteredKinds.length > 0 ? (
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
.root {
border-radius: var(--mantine-radius-lg);
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
}

.item {
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
border: 1px solid transparent;
position: relative;
z-index: 0;
transition: transform 150ms ease;
overflow: hidden;

&[data-first="true"] {
border-radius: var(--mantine-radius-lg) var(--mantine-radius-lg) 0 0;
}

&[data-last="true"] {
border-radius: 0 0 var(--mantine-radius-lg) var(--mantine-radius-lg);
}

&[data-active] {
transform: scale(1.01);
z-index: 1;
background-color: var(--mantine-color-body);
border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-4));
box-shadow: var(--mantine-shadow-md);
border-radius: var(--mantine-radius-lg);
}
}

.chevron {
&[data-rotate] {
transform: rotate(180deg);
}
}
16 changes: 11 additions & 5 deletions apps/nextjs/src/app/[locale]/manage/integrations/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { NoResults } from "~/components/no-results";
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
import { DeleteIntegrationActionButton } from "./_integration-buttons";
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
import classes from "./page.module.css";

interface IntegrationsPageProps {
searchParams: Promise<{
Expand Down Expand Up @@ -125,7 +126,7 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
return <NoResults icon={IconPlugX} title={t("page.list.noResults.title")} />;
}

const grouppedIntegrations = integrations.reduce(
const groupedIntegrations = integrations.reduce(
(acc, integration) => {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (!acc[integration.kind]) {
Expand All @@ -140,10 +141,15 @@ const IntegrationList = async ({ integrations, activeTab }: IntegrationListProps
);

return (
<ActiveTabAccordion defaultValue={activeTab} variant="separated">
{objectEntries(grouppedIntegrations).map(([kind, integrations]) => (
<AccordionItem key={kind} value={kind}>
<AccordionControl icon={<IntegrationAvatar size="sm" kind={kind} />}>
<ActiveTabAccordion defaultValue={activeTab} radius="lg" classNames={classes}>
{objectEntries(groupedIntegrations).map(([kind, integrations], index) => (
<AccordionItem
key={kind}
value={kind}
data-first={index === 0}
data-last={index === objectEntries(groupedIntegrations).length - 1}
>
<AccordionControl icon={<IntegrationAvatar size="sm" kind={kind} radius="sm" />}>
<Group>
<Text>{getIntegrationName(kind)}</Text>
<CountBadge count={integrations.length} />
Expand Down
Loading
Loading