diff --git a/apps/docs/docs/widgets/media-request-list/index.mdx b/apps/docs/docs/widgets/media-request-list/index.mdx index 2ed31ea88..9a4599a3d 100644 --- a/apps/docs/docs/widgets/media-request-list/index.mdx +++ b/apps/docs/docs/widgets/media-request-list/index.mdx @@ -16,6 +16,7 @@ import TabItem from "@theme/TabItem"; import { mediaRequestListWidget } from "."; import { jellyseerrIntegration } from "@site/docs/integrations/jellyseerr"; import { overseerrIntegration } from "@site/docs/integrations/overseerr"; +import { seerrIntegration } from "@site/docs/integrations/seerr"; @@ -39,6 +40,9 @@ import mediaRequestList from "./img/list.png"; { integration: overseerrIntegration, }, + { + integration: seerrIntegration, + }, ]} /> @@ -46,6 +50,7 @@ import mediaRequestList from "./img/list.png"; - Approve / decline a request by clicking on the thumbs up or down icon. - Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations. +- When no requests exist yet, the widget offers the same media search action instead of reporting an integration error. - A warning indicator identifies unavailable integrations without hiding requests returned by healthy integrations. diff --git a/apps/docs/docs/widgets/media-request-stats/index.mdx b/apps/docs/docs/widgets/media-request-stats/index.mdx index 7376bb0e4..7a06bf365 100644 --- a/apps/docs/docs/widgets/media-request-stats/index.mdx +++ b/apps/docs/docs/widgets/media-request-stats/index.mdx @@ -16,6 +16,7 @@ import TabItem from "@theme/TabItem"; import { mediaRequestStatsWidget } from "."; import { jellyseerrIntegration } from "@site/docs/integrations/jellyseerr"; import { overseerrIntegration } from "@site/docs/integrations/overseerr"; +import { seerrIntegration } from "@site/docs/integrations/seerr"; @@ -31,6 +32,9 @@ Provides an overview of media requests, including totals, availability, approval { integration: overseerrIntegration, }, + { + integration: seerrIntegration, + }, ]} /> @@ -43,6 +47,7 @@ import mediaRequestStats from "./img/stats.png"; ### Interactions - Hover over the widget to reveal a **search button** that opens Spotlight in media request search mode, scoped to the widget's integrations. +- When no requests exist yet, the widget offers media search and explains when statistics will appear. - A warning indicator identifies unavailable integrations while totals from healthy integrations remain visible. diff --git a/packages/definitions/src/widget.ts b/packages/definitions/src/widget.ts index 5279df2e0..f8df10772 100644 --- a/packages/definitions/src/widget.ts +++ b/packages/definitions/src/widget.ts @@ -63,13 +63,19 @@ export type WidgetKind = (typeof widgetKinds)[number]; export const widgetDefaultSizes: Partial> = { airQuality: { width: 2, height: 1 }, + calendar: { width: 2, height: 2 }, countdown: { width: 2, height: 1 }, + downloads: { width: 4, height: 2 }, + indexerManager: { width: 2, height: 3 }, timer: { width: 2, height: 1 }, uptimeKuma: { width: 2, height: 3 }, audioStats: { width: 2, height: 2 }, paperlessNgx: { width: 2, height: 2 }, patchmon: { width: 2, height: 2 }, - mediaMissing: { width: 4, height: 3 }, + mediaServer: { width: 3, height: 2 }, + "mediaRequests-requestList": { width: 3, height: 2 }, + "mediaRequests-requestStats": { width: 2, height: 2 }, + mediaMissing: { width: 2, height: 2 }, bazarr: { width: 2, height: 2 }, assistant: { width: 10, height: 4 }, }; diff --git a/packages/translation/src/lang/en.json b/packages/translation/src/lang/en.json index 5ef1cee93..04dabe9f7 100644 --- a/packages/translation/src/lang/en.json +++ b/packages/translation/src/lang/en.json @@ -4227,7 +4227,11 @@ }, "mediaRequests-requestList": { "name": "Media Requests List", - "description": "See a list of all media requests from your Overseerr or Jellyseerr instance", + "description": "See a list of all media requests from your Seerr, Overseerr, or Jellyseerr instance", + "empty": { + "title": "No media requests yet", + "description": "Search for a movie or show to start a request." + }, "option": { "linksTargetNewTab": { "label": "Open links in new tab" @@ -4267,6 +4271,10 @@ "mediaRequests-requestStats": { "name": "Media Requests Stats", "description": "Statistics about your media requests", + "empty": { + "title": "No request activity yet", + "description": "Stats will appear after the first media request." + }, "titles": { "stats": { "main": "Media Stats", diff --git a/packages/widgets/src/media-requests/empty-state.tsx b/packages/widgets/src/media-requests/empty-state.tsx new file mode 100644 index 000000000..20752a18b --- /dev/null +++ b/packages/widgets/src/media-requests/empty-state.tsx @@ -0,0 +1,49 @@ +"use client"; + +import { Button, Stack, Text, ThemeIcon } from "@mantine/core"; +import { IconMovieOff, IconSearch } from "@tabler/icons-react"; + +import { openMediaRequestSearch } from "@homarr/spotlight"; +import { useI18n } from "@homarr/translation/client"; + +interface MediaRequestsEmptyStateProps { + title: string; + description: string; + integrationIds: string[]; + isEditMode: boolean; +} + +export const MediaRequestsEmptyState = ({ + title, + description, + integrationIds, + isEditMode, +}: MediaRequestsEmptyStateProps) => { + const tSearch = useI18n("search.mode.media"); + + return ( + + + + + + + {title} + + + {description} + + + {!isEditMode ? ( + + ) : null} + + ); +}; diff --git a/packages/widgets/src/media-requests/list/component.tsx b/packages/widgets/src/media-requests/list/component.tsx index 617b332be..59d2e14be 100644 --- a/packages/widgets/src/media-requests/list/component.tsx +++ b/packages/widgets/src/media-requests/list/component.tsx @@ -20,7 +20,7 @@ import { WidgetQueryLoadingState } from "../../common/query-state-indicator"; import actionTargetClasses from "../../common/action-target.module.css"; import { IntegrationErrorIndicator } from "../../common/integration-error-indicator"; import type { WidgetComponentProps } from "../../definition"; -import { NoIntegrationDataError } from "../../errors/no-data-integration"; +import { MediaRequestsEmptyState } from "../empty-state"; import classes from "./component.module.css"; import searchClasses from "../search-button.module.css"; @@ -30,6 +30,7 @@ export default function MediaServerWidget({ options, width, }: WidgetComponentProps<"mediaRequests-requestList">) { + const t = useI18n("widget.mediaRequests-requestList"); const interactIntegrationIds = new Set( useIntegrationsWithInteractAccess() .filter(({ id }) => integrationIds.includes(id)) @@ -48,7 +49,16 @@ export default function MediaServerWidget({ if (isInitialWidgetQueryPending(mediaRequestQuery)) return ; if (!mediaRequestData) return ; const { requests: mediaRequests, failedIntegrations } = mediaRequestData; - if (mediaRequests.length === 0 && failedIntegrations.length === 0) throw new NoIntegrationDataError(); + if (mediaRequests.length === 0 && failedIntegrations.length === 0) { + return ( + + ); + } const showIntegrationSource = new Set(mediaRequests.map(({ integrationId }) => integrationId)).size > 1; return ( diff --git a/packages/widgets/src/media-requests/stats/component.tsx b/packages/widgets/src/media-requests/stats/component.tsx index 92aaac4c6..074014264 100644 --- a/packages/widgets/src/media-requests/stats/component.tsx +++ b/packages/widgets/src/media-requests/stats/component.tsx @@ -28,7 +28,7 @@ import { WidgetQueryLoadingState } from "../../common/query-state-indicator"; import actionTargetClasses from "../../common/action-target.module.css"; import { IntegrationErrorIndicator } from "../../common/integration-error-indicator"; import type { WidgetComponentProps } from "../../definition"; -import { NoIntegrationDataError } from "../../errors/no-data-integration"; +import { MediaRequestsEmptyState } from "../empty-state"; import classes from "./component.module.css"; import searchClasses from "../search-button.module.css"; @@ -54,8 +54,16 @@ export default function MediaServerWidget({ requestStats.users.length === 0 && requestStats.stats.length === 0 && requestStats.failedIntegrations.length === 0 - ) - throw new NoIntegrationDataError(); + ) { + return ( + + ); + } const data = [ {