Files
homarr/packages/modals-collection/src/apps/app-select-modal.tsx
2025-04-11 18:55:00 +00:00

122 lines
4.0 KiB
TypeScript

import { useMemo, useState } from "react";
import Image from "next/image";
import { Button, Card, Center, Grid, Input, Stack, Text } from "@mantine/core";
import { IconPlus, IconSearch } from "@tabler/icons-react";
import { clientApi } from "@homarr/api/client";
import { createModal, useModalAction } from "@homarr/modals";
import { useI18n } from "@homarr/translation/client";
import { QuickAddAppModal } from "./quick-add-app/quick-add-app-modal";
interface AppSelectModalProps {
onSelect?: (appId: string) => void;
}
export const AppSelectModal = createModal<AppSelectModalProps>(({ actions, innerProps }) => {
const [search, setSearch] = useState("");
const t = useI18n();
const { data: apps = [], isPending } = clientApi.app.selectable.useQuery();
const { openModal: openQuickAddAppModal } = useModalAction(QuickAddAppModal);
const filteredApps = useMemo(
() =>
apps
.filter((app) => app.name.toLowerCase().includes(search.toLowerCase()))
.sort((a, b) => a.name.localeCompare(b.name)),
[apps, search],
);
const handleSelect = (appId: string) => {
if (innerProps.onSelect) {
innerProps.onSelect(appId);
}
actions.closeModal();
};
const handleAddNewApp = () => {
openQuickAddAppModal({
onClose(createdAppId) {
if (innerProps.onSelect) {
innerProps.onSelect(createdAppId);
}
actions.closeModal();
},
});
};
return (
<Stack>
<Input
value={search}
onChange={(event) => setSearch(event.currentTarget.value)}
leftSection={<IconSearch />}
placeholder={`${t("app.action.select.search")}...`}
data-autofocus
onKeyDown={(event) => {
if (event.key === "Enter" && filteredApps.length === 1 && filteredApps[0]) {
handleSelect(filteredApps[0].id);
}
}}
/>
<Grid>
<Grid.Col span={{ xs: 12, sm: 4, md: 3 }}>
<Card h="100%">
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<IconPlus size={24} />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{t("app.action.create.title")}
</Text>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
{t("app.action.create.description")}
</Text>
</Stack>
<Button onClick={handleAddNewApp} variant="light" size="xs" mt="auto" radius="md" fullWidth>
{t("app.action.create.action")}
</Button>
</Stack>
</Card>
</Grid.Col>
{filteredApps.map((app) => (
<Grid.Col key={app.id} span={{ xs: 12, sm: 4, md: 3 }}>
<Card h="100%">
<Stack justify="space-between" h="100%">
<Stack gap="xs">
<Center>
<Image src={app.iconUrl || ""} alt={app.name} width={24} height={24} />
</Center>
<Text lh={1.2} style={{ whiteSpace: "normal" }} ta="center">
{app.name}
</Text>
<Text lh={1.2} style={{ whiteSpace: "normal" }} size="xs" ta="center" c="dimmed">
{app.description ?? ""}
</Text>
</Stack>
<Button onClick={() => handleSelect(app.id)} variant="light" size="xs" mt="auto" radius="md" fullWidth>
{t("app.action.select.action", { app: app.name })}
</Button>
</Stack>
</Card>
</Grid.Col>
))}
{filteredApps.length === 0 && !isPending && (
<Grid.Col span={12}>
<Center p="xl">
<Text c="dimmed">{t("app.action.select.noResults")}</Text>
</Center>
</Grid.Col>
)}
</Grid>
</Stack>
);
}).withOptions({
defaultTitle: (t) => t("app.action.select.title"),
size: "xl",
});