From 1e9098d052801fe7d53ce15e9f22ed89de04d644 Mon Sep 17 00:00:00 2001 From: Meier Lukas Date: Sat, 4 Jan 2025 22:43:30 +0100 Subject: [PATCH] feat(icons): add upload button to icon picker (#1859) --- .../manage/medias/_actions/upload-media.tsx | 41 ++++++-- .../src/app/[locale]/manage/medias/page.tsx | 4 +- .../components/icons/picker/icon-picker.tsx | 95 ++++++++++++------- 3 files changed, 96 insertions(+), 44 deletions(-) diff --git a/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx b/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx index 72b880fc9..f8164cdac 100644 --- a/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx +++ b/apps/nextjs/src/app/[locale]/manage/medias/_actions/upload-media.tsx @@ -1,15 +1,40 @@ "use client"; +import type { JSX } from "react"; import { Button, FileButton } from "@mantine/core"; import { IconUpload } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; import { revalidatePathActionAsync } from "@homarr/common/client"; +import type { MaybePromise } from "@homarr/common/types"; import { showErrorNotification, showSuccessNotification } from "@homarr/notifications"; import { useI18n } from "@homarr/translation/client"; import { supportedMediaUploadFormats } from "@homarr/validation"; -export const UploadMedia = () => { +export const UploadMediaButton = () => { + const t = useI18n(); + const onSettledAsync = async () => { + await revalidatePathActionAsync("/manage/medias"); + }; + + return ( + + {({ onClick, loading }) => ( + + )} + + ); +}; + +interface UploadMediaProps { + children: (props: { onClick: () => void; loading: boolean }) => JSX.Element; + onSettled?: () => MaybePromise; + onSuccess?: (media: { id: string; url: string }) => MaybePromise; +} + +export const UploadMedia = ({ children, onSettled, onSuccess }: UploadMediaProps) => { const t = useI18n(); const { mutateAsync, isPending } = clientApi.media.uploadMedia.useMutation(); @@ -18,10 +43,14 @@ export const UploadMedia = () => { const formData = new FormData(); formData.append("file", file); await mutateAsync(formData, { - onSuccess() { + async onSuccess(mediaId) { showSuccessNotification({ message: t("media.action.upload.notification.success.message"), }); + await onSuccess?.({ + id: mediaId, + url: `/api/user-medias/${mediaId}`, + }); }, onError() { showErrorNotification({ @@ -29,18 +58,14 @@ export const UploadMedia = () => { }); }, async onSettled() { - await revalidatePathActionAsync("/manage/medias"); + await onSettled?.(); }, }); }; return ( - {({ onClick }) => ( - - )} + {({ onClick }) => children({ onClick, loading: isPending })} ); }; diff --git a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx index 17c01f519..ed50a18ad 100644 --- a/apps/nextjs/src/app/[locale]/manage/medias/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/medias/page.tsx @@ -16,7 +16,7 @@ import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { CopyMedia } from "./_actions/copy-media"; import { DeleteMedia } from "./_actions/delete-media"; import { IncludeFromAllUsersSwitch } from "./_actions/show-all"; -import { UploadMedia } from "./_actions/upload-media"; +import { UploadMediaButton } from "./_actions/upload-media"; const searchParamsSchema = z.object({ search: z.string().optional(), @@ -61,7 +61,7 @@ export default async function GroupsListPage(props: MediaListPageProps) { )} - {session.user.permissions.includes("media-upload") && } + {session.user.permissions.includes("media-upload") && } diff --git a/apps/nextjs/src/components/icons/picker/icon-picker.tsx b/apps/nextjs/src/components/icons/picker/icon-picker.tsx index 432142dbf..d0758a66e 100644 --- a/apps/nextjs/src/components/icons/picker/icon-picker.tsx +++ b/apps/nextjs/src/components/icons/picker/icon-picker.tsx @@ -1,10 +1,12 @@ import type { FocusEventHandler } from "react"; import { startTransition, useState } from "react"; import { + ActionIcon, Box, Card, Combobox, Flex, + Group, Image, Indicator, InputBase, @@ -16,10 +18,13 @@ import { useCombobox, } from "@mantine/core"; import { useDebouncedValue } from "@mantine/hooks"; +import { IconUpload } from "@tabler/icons-react"; import { clientApi } from "@homarr/api/client"; +import { useSession } from "@homarr/auth/client"; import { useScopedI18n } from "@homarr/translation/client"; +import { UploadMedia } from "~/app/[locale]/manage/medias/_actions/upload-media"; import classes from "./icon-picker.module.css"; interface IconPickerProps { @@ -34,6 +39,7 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I const [value, setValue] = useState(initialValue ?? ""); const [search, setSearch] = useState(initialValue ?? ""); const [previewUrl, setPreviewUrl] = useState(initialValue ?? null); + const { data: session } = useSession(); const tCommon = useScopedI18n("common"); @@ -105,40 +111,61 @@ export const IconPicker = ({ initialValue, onChange, error, onFocus, onBlur }: I return ( - } - leftSection={ - previewUrl ? ( - // eslint-disable-next-line @next/next/no-img-element - - ) : null - } - value={search} - onChange={(event) => { - combobox.openDropdown(); - combobox.updateSelectedOptionIndex(); - setSearch(event.currentTarget.value); - setValue(event.currentTarget.value); - setPreviewUrl(null); - onChange(event.currentTarget.value); - }} - onClick={() => combobox.openDropdown()} - onFocus={(event) => { - onFocus?.(event); - combobox.openDropdown(); - }} - onBlur={(event) => { - onBlur?.(event); - combobox.closeDropdown(); - setPreviewUrl(value); - setSearch(value || ""); - }} - rightSectionPointerEvents="none" - withAsterisk - error={error} - label={tCommon("iconPicker.label")} - placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })} - /> + + } + leftSection={ + previewUrl ? ( + // eslint-disable-next-line @next/next/no-img-element + + ) : null + } + value={search} + onChange={(event) => { + combobox.openDropdown(); + combobox.updateSelectedOptionIndex(); + setSearch(event.currentTarget.value); + setValue(event.currentTarget.value); + setPreviewUrl(null); + onChange(event.currentTarget.value); + }} + onClick={() => combobox.openDropdown()} + onFocus={(event) => { + onFocus?.(event); + combobox.openDropdown(); + }} + onBlur={(event) => { + onBlur?.(event); + combobox.closeDropdown(); + setPreviewUrl(value); + setSearch(value || ""); + }} + rightSectionPointerEvents="none" + withAsterisk + error={error} + label={tCommon("iconPicker.label")} + placeholder={tCommon("iconPicker.header", { countIcons: data?.countIcons ?? 0 })} + /> + {session?.user.permissions.includes("media-upload") && ( + { + startTransition(() => { + setValue(url); + setPreviewUrl(url); + setSearch(url); + onChange(url); + }); + }} + > + {({ onClick, loading }) => ( + + + + )} + + )} +