diff --git a/src/components/AiringChannelCountryBadge.tsx b/src/components/AiringChannelCountryBadge.tsx new file mode 100644 index 000000000..e99cea282 --- /dev/null +++ b/src/components/AiringChannelCountryBadge.tsx @@ -0,0 +1,26 @@ +// One shared formatter: the names come from the browser, in English like the rest of the WebUI. +const regionNames = new Intl.DisplayNames(['en'], { type: 'region', fallback: 'none' }); + +const getCountryName = (countryCode: string) => { + try { + return regionNames.of(countryCode) ?? countryCode; + } catch { + return countryCode; + } +}; + +/** A channel's country as a small badge with its two-letter code, named in the tooltip. Nothing without a country. */ +const AiringChannelCountryBadge = ({ countryCode }: { countryCode: string | null | undefined }) => { + if (!countryCode) return null; + return ( + + {countryCode.toUpperCase()} + + ); +}; + +export default AiringChannelCountryBadge; diff --git a/src/components/AiringProviderIcon.tsx b/src/components/AiringProviderIcon.tsx new file mode 100644 index 000000000..1d09a80c8 --- /dev/null +++ b/src/components/AiringProviderIcon.tsx @@ -0,0 +1,50 @@ +import { useState } from 'react'; +import { mdiDatabaseOutline } from '@mdi/js'; +import { Icon } from '@mdi/react'; +import cx from 'classnames'; + +import { getAiringProviderIconUrl } from '@/core/react-query/airing-schedule/helpers'; + +type Props = { + className?: string; + /** Whether the server has an icon for the provider, from the provider's `HasIcon`. */ + hasIcon: boolean; + /** + * The provider's name, for the tooltip and the accessible label where the icon stands in for the name. Left out + * where the name is shown next to the icon. + */ + label?: string; + providerId: string; + /** In `@mdi/react` units: 1 is 1.5rem. */ + size?: number; +}; + +/** An airing provider's icon from the server, or the generic icon the metadata sources use when it has none. */ +const AiringProviderIcon = ({ className, hasIcon, label, providerId, size = 1 }: Props) => { + const [failed, setFailed] = useState(false); + const labelProps = label + ? { 'aria-label': label, 'data-tooltip-id': 'tooltip', 'data-tooltip-content': label, role: 'img' } + : { 'aria-hidden': true }; + + if (!hasIcon || failed) { + return ( + + + + ); + } + + return ( + {label setFailed(true)} + src={getAiringProviderIconUrl(providerId)} + style={{ width: `${size * 1.5}rem`, height: `${size * 1.5}rem` }} + /> + ); +}; + +export default AiringProviderIcon; diff --git a/src/components/BackgroundImagePlaceholderDiv.tsx b/src/components/BackgroundImagePlaceholderDiv.tsx index 3979a8215..211f745e8 100644 --- a/src/components/BackgroundImagePlaceholderDiv.tsx +++ b/src/components/BackgroundImagePlaceholderDiv.tsx @@ -12,6 +12,8 @@ type Props = { children?: ReactNode; className?: string; contain?: boolean; + /** Where the image sits in its frame, as a CSS `background-position`. Defaults to `center`. */ + position?: string; image?: ImageLinkType; hidePlaceholderOnHover?: boolean; overlayOnHover?: boolean; @@ -30,6 +32,7 @@ const BackgroundImagePlaceholderDiv = (props: Props) => { inCollection, linkToImage, overlayOnHover, + position = 'center', zoomOnHover, } = props; const fit = contain ? 'contain' : 'cover'; @@ -92,7 +95,9 @@ const BackgroundImagePlaceholderDiv = (props: Props) => { 'absolute top-0 left-0 z-[-1] flex size-full flex-col rounded-lg text-center', zoomOnHover && 'transition-transform duration-600 group-hover:scale-105', )} - style={{ background: backgroundImage ? `center / ${fit} no-repeat url('${backgroundImage.src}')` : undefined }} + style={{ + background: backgroundImage ? `${position} / ${fit} no-repeat url('${backgroundImage.src}')` : undefined, + }} > {imageError && (
void; + /** Whether several languages can be picked, as with a `