mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-15 16:20:03 +00:00
Extend Plugin API to return a list of covers and header images
Implement dedicated header image in GameView Implement GameHeaderPicker
This commit is contained in:
@@ -1,13 +1,12 @@
|
||||
import {Image, useDisclosure} from "@heroui/react";
|
||||
import {GameCoverFallback} from "Frontend/components/general/covers/GameCoverFallback";
|
||||
import React from "react";
|
||||
import {useField} from "formik";
|
||||
import {GameCoverPickerModal} from "Frontend/components/general/modals/GameCoverPickerModal";
|
||||
import {Pencil} from "@phosphor-icons/react";
|
||||
import {ImageBroken, Pencil} from "@phosphor-icons/react";
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
export default function GameCoverPicker({game, label, showErrorUntouched = false, ...props}) {
|
||||
export default function GameCoverPicker({game, showErrorUntouched = false, ...props}) {
|
||||
|
||||
// @ts-ignore
|
||||
const [field] = useField(props);
|
||||
@@ -15,24 +14,30 @@ export default function GameCoverPicker({game, label, showErrorUntouched = false
|
||||
const gameCoverPickerModal = useDisclosure();
|
||||
|
||||
return (<>
|
||||
<div className="relative group w-fit h-fit cursor-pointer"
|
||||
<div className="relative group aspect-[12/17] cursor-pointer bg-background/50"
|
||||
onClick={gameCoverPickerModal.onOpenChange}>
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 object-cover aspect-[12/17] group-hover:brightness-50"
|
||||
src={field.value ? field.value : `images/cover/${game.coverId}`}
|
||||
{...props}
|
||||
{...field}
|
||||
radius="none"
|
||||
height={216}
|
||||
fallbackSrc={<GameCoverFallback title={game.title}
|
||||
size={216}
|
||||
radius="none"/>}
|
||||
/>
|
||||
{field.value || game.coverId ?
|
||||
<div className="size-full overflow-hidden">
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 object-cover group-hover:brightness-[25%]"
|
||||
src={field.value ? field.value : `images/cover/${game.coverId}`}
|
||||
{...props}
|
||||
{...field}
|
||||
radius="none"
|
||||
/>
|
||||
</div> :
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col text-center items-center justify-center group-hover:opacity-0"
|
||||
>
|
||||
<ImageBroken size={46}/>
|
||||
<p>No cover image available</p>
|
||||
</div>}
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
className="absolute inset-0 flex flex-col gap-2 text-center items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Pencil size={46}/>
|
||||
<p>Edit cover</p>
|
||||
</div>
|
||||
</div>
|
||||
<GameCoverPickerModal
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import {Image, useDisclosure} from "@heroui/react";
|
||||
import React from "react";
|
||||
import {useField} from "formik";
|
||||
import {ImageBroken, Pencil} from "@phosphor-icons/react";
|
||||
import {GameHeaderPickerModal} from "Frontend/components/general/modals/GameHeaderPickerModal";
|
||||
|
||||
|
||||
// @ts-ignore
|
||||
export default function GameHeaderPicker({game, showErrorUntouched = false, ...props}) {
|
||||
|
||||
// @ts-ignore
|
||||
const [field] = useField(props);
|
||||
|
||||
const gameHeaderPickerModal = useDisclosure();
|
||||
|
||||
return (<>
|
||||
<div className="relative group size-full cursor-pointer bg-background/50"
|
||||
onClick={gameHeaderPickerModal.onOpenChange}>
|
||||
{field.value || game.headerId ?
|
||||
<div className="size-full overflow-hidden">
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 object-cover group-hover:brightness-[25%]"
|
||||
src={field.value ? field.value : `images/cover/${game.headerId}`}
|
||||
{...props}
|
||||
{...field}
|
||||
radius="none"
|
||||
/>
|
||||
</div> :
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col text-center items-center justify-center group-hover:opacity-0"
|
||||
>
|
||||
<ImageBroken size={46}/>
|
||||
<p>No header image available</p>
|
||||
</div>}
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col gap-2 text-center items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<Pencil size={46}/>
|
||||
<p>Edit header image</p>
|
||||
</div>
|
||||
</div>
|
||||
<GameHeaderPickerModal
|
||||
game={game}
|
||||
isOpen={gameHeaderPickerModal.isOpen}
|
||||
onOpenChange={gameHeaderPickerModal.onOpenChange}
|
||||
setHeaderUrl={(headerUrl) => field.onChange({target: {name: field.name, value: headerUrl}})}
|
||||
/>
|
||||
</>);
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import * as Yup from "yup";
|
||||
import GameCoverPicker from "Frontend/components/general/input/GameCoverPicker";
|
||||
import DatePickerInput from "Frontend/components/general/input/DatePickerInput";
|
||||
import ArrayInput from "Frontend/components/general/input/ArrayInput";
|
||||
import GameHeaderPicker from "Frontend/components/general/input/GameHeaderPicker";
|
||||
|
||||
interface EditGameMetadataModalProps {
|
||||
game: GameDto;
|
||||
@@ -57,15 +58,16 @@ export default function EditGameMetadataModal({game, isOpen, onOpenChange}: Edit
|
||||
Update game metadata
|
||||
</ModalHeader>
|
||||
<ModalBody>
|
||||
<div className="flex flex-row gap-8">
|
||||
{/*@ts-ignore*/}
|
||||
<Input key="metadata.path" name="metadata.path" label="Path"
|
||||
isDisabled className="mb-0"/>
|
||||
<div className="flex flex-row gap-4 h-44">
|
||||
<GameCoverPicker key="coverUrl" name="coverUrl" game={game}/>
|
||||
<div className="flex flex-col flex-1">
|
||||
<Input key="metadata.path" name="metadata.path" label="Path"
|
||||
isDisabled/>
|
||||
<Input key="title" name="title" label="Title" isRequired/>
|
||||
<DatePickerInput key="release" name="release" label="Release"/>
|
||||
</div>
|
||||
<GameHeaderPicker key="headerUrl" name="headerUrl" game={game}/>
|
||||
</div>
|
||||
<div className="flex flex-row gap-4">
|
||||
<Input key="title" name="title" label="Title" isRequired/>
|
||||
<DatePickerInput key="release" name="release" label="Release"
|
||||
className="w-fit"/>
|
||||
</div>
|
||||
<TextAreaInput key="summary" name="summary" label="Summary (HTML)"/>
|
||||
<TextAreaInput key="comment" name="comment" label="Comment (Markdown)"/>
|
||||
|
||||
@@ -4,6 +4,10 @@ import React, {useEffect, useState} from "react";
|
||||
import GameSearchResultDto from "Frontend/generated/org/gameyfin/app/games/dto/GameSearchResultDto";
|
||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||
import {ArrowRight, MagnifyingGlass} from "@phosphor-icons/react";
|
||||
import PluginIcon from "Frontend/components/general/plugin/PluginIcon";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {pluginState} from "Frontend/state/PluginState";
|
||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||
|
||||
interface GameCoverPickerModalProps {
|
||||
game: GameDto;
|
||||
@@ -17,7 +21,9 @@ export function GameCoverPickerModal({game, isOpen, onOpenChange, setCoverUrl}:
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState(game.title);
|
||||
const [searchResults, setSearchResults] = useState<GameSearchResultDto[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false)
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const state = useSnapshot(pluginState).state;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchTerm.length > 0 && searchResults.length === 0) {
|
||||
@@ -27,8 +33,8 @@ export function GameCoverPickerModal({game, isOpen, onOpenChange, setCoverUrl}:
|
||||
|
||||
async function search() {
|
||||
setIsSearching(true);
|
||||
const results = await GameEndpoint.getPotentialMatches(searchTerm, false);
|
||||
let validResults = results.filter(result => result.coverUrl && result.coverUrl.length > 0 && result.coverUrl !== "null");
|
||||
const results = await GameEndpoint.getPotentialMatches(searchTerm);
|
||||
let validResults = results.filter(result => result.coverUrls && result.coverUrls.length > 0);
|
||||
setSearchResults(validResults);
|
||||
setIsSearching(false);
|
||||
}
|
||||
@@ -78,25 +84,36 @@ export function GameCoverPickerModal({game, isOpen, onOpenChange, setCoverUrl}:
|
||||
<p className="text-center text-foreground/70">Searching...</p>
|
||||
}
|
||||
<ScrollShadow
|
||||
className="grid grid-cols-auto-fill gap-4 h-96 overflow-scroll justify-evenly">
|
||||
{searchResults.map((result) => (
|
||||
<div className="relative group w-fit h-fit cursor-pointer"
|
||||
className="grid grid-cols-auto-fill gap-4 h-96 overflow-y-scroll justify-evenly">
|
||||
{searchResults.flatMap(result => {
|
||||
if (!result.coverUrls) return [];
|
||||
return result.coverUrls.map((url, idx) => ({
|
||||
id: `${result.id}-${idx}`,
|
||||
title: result.title,
|
||||
url: url.url,
|
||||
source: url.pluginId
|
||||
}))
|
||||
}).map(cover => (
|
||||
<div key={cover.id}
|
||||
className="relative group w-fit h-fit cursor-pointer"
|
||||
onClick={() => {
|
||||
setCoverUrl(result.coverUrl!);
|
||||
onClose();
|
||||
}}>
|
||||
setCoverUrl(cover.url);
|
||||
onOpenChange();
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
key={result.id}
|
||||
alt={result.title}
|
||||
className="z-0 object-cover aspect-[12/17] group-hover:brightness-50"
|
||||
src={result.coverUrl!}
|
||||
alt={cover.title}
|
||||
className="z-0 object-cover aspect-[12/17] group-hover:brightness-[25%]"
|
||||
src={cover.url}
|
||||
radius="none"
|
||||
height={216}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 flex items-center justify-center opacity-0 group-hover:opacity-100"
|
||||
>
|
||||
<ArrowRight size={46}/>
|
||||
className="absolute inset-0 flex flex-col gap-4 items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<PluginIcon plugin={state[cover.source] as PluginDto} size={32}
|
||||
blurred={false} showTooltip={false}/>
|
||||
<p className="text-s text-center">{cover.title}</p>
|
||||
<ArrowRight/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
import GameDto from "Frontend/generated/org/gameyfin/app/games/dto/GameDto";
|
||||
import {Button, Image, Input, Modal, ModalBody, ModalContent, ModalHeader, ScrollShadow} from "@heroui/react";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import GameSearchResultDto from "Frontend/generated/org/gameyfin/app/games/dto/GameSearchResultDto";
|
||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||
import {ArrowRight, MagnifyingGlass} from "@phosphor-icons/react";
|
||||
import PluginIcon from "Frontend/components/general/plugin/PluginIcon";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {pluginState} from "Frontend/state/PluginState";
|
||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||
|
||||
interface GameHeaderPickerModalProps {
|
||||
game: GameDto;
|
||||
isOpen: boolean;
|
||||
onOpenChange: () => void;
|
||||
setHeaderUrl: (url: string) => void;
|
||||
}
|
||||
|
||||
export function GameHeaderPickerModal({game, isOpen, onOpenChange, setHeaderUrl}: GameHeaderPickerModalProps) {
|
||||
const [headerUrl, setHeaderUrlState] = useState("");
|
||||
|
||||
const [searchTerm, setSearchTerm] = useState(game.title);
|
||||
const [searchResults, setSearchResults] = useState<GameSearchResultDto[]>([]);
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
|
||||
const state = useSnapshot(pluginState).state;
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && searchTerm.length > 0 && searchResults.length === 0) {
|
||||
search();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
async function search() {
|
||||
setIsSearching(true);
|
||||
const results = await GameEndpoint.getPotentialMatches(searchTerm);
|
||||
let validResults = results.filter(result => result.headerUrls && result.headerUrls.length > 0);
|
||||
setSearchResults(validResults);
|
||||
setIsSearching(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal isOpen={isOpen} onOpenChange={onOpenChange} backdrop="opaque" size="2xl">
|
||||
<ModalContent>
|
||||
{(onClose) => {
|
||||
return (<>
|
||||
<ModalHeader>
|
||||
Enter a URL or search for a header
|
||||
</ModalHeader>
|
||||
<ModalBody className="flex flex-col gap-4">
|
||||
<div className="flex flex-row gap-2 mb-4">
|
||||
<Input isClearable
|
||||
placeholder="Enter a URL"
|
||||
value={headerUrl}
|
||||
onValueChange={setHeaderUrlState}
|
||||
onClear={() => setHeaderUrlState("")}
|
||||
/>
|
||||
<Button isIconOnly onPress={() => {
|
||||
setHeaderUrl(headerUrl);
|
||||
onClose();
|
||||
}}>
|
||||
<ArrowRight/>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 mb-4">
|
||||
<Input placeholder="Search"
|
||||
value={searchTerm}
|
||||
onValueChange={setSearchTerm}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
await search();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button isIconOnly onPress={search} color="primary" isLoading={isSearching}>
|
||||
<MagnifyingGlass/>
|
||||
</Button>
|
||||
</div>
|
||||
{searchResults.length === 0 && !isSearching &&
|
||||
<p className="text-center">No results found.</p>
|
||||
}
|
||||
{searchResults.length === 0 && isSearching &&
|
||||
<p className="text-center text-foreground/70">Searching...</p>
|
||||
}
|
||||
<ScrollShadow
|
||||
className="flex flex-col items-center gap-4 h-96 overflow-y-scroll">
|
||||
{searchResults.flatMap(result => {
|
||||
if (!result.headerUrls) return [];
|
||||
return result.headerUrls.map((url, idx) => ({
|
||||
id: `${result.id}-${idx}`,
|
||||
title: result.title,
|
||||
url: url.url,
|
||||
source: url.pluginId
|
||||
}))
|
||||
}).map(header => (
|
||||
<div key={header.id}
|
||||
className="relative group w-fit h-fit cursor-pointer"
|
||||
onClick={() => {
|
||||
setHeaderUrl(header.url);
|
||||
onOpenChange();
|
||||
}}
|
||||
>
|
||||
<Image
|
||||
alt={header.title}
|
||||
className="z-0 object-cover group-hover:brightness-[25%]"
|
||||
src={header.url}
|
||||
radius="none"
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 flex flex-col gap-4 items-center justify-center opacity-0 group-hover:opacity-100">
|
||||
<PluginIcon plugin={state[header.source] as PluginDto} size={32}
|
||||
blurred={false} showTooltip={false}/>
|
||||
<p className="text-s text-center">{header.title}</p>
|
||||
<ArrowRight/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</ScrollShadow>
|
||||
</ModalBody>
|
||||
</>)
|
||||
}}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
@@ -17,6 +17,9 @@ import {ArrowRight, MagnifyingGlass} from "@phosphor-icons/react";
|
||||
import {GameEndpoint} from "Frontend/generated/endpoints";
|
||||
import GameSearchResultDto from "Frontend/generated/org/gameyfin/app/games/dto/GameSearchResultDto";
|
||||
import PluginIcon from "../plugin/PluginIcon";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import {pluginState} from "Frontend/state/PluginState";
|
||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||
|
||||
interface EditGameMetadataModalProps {
|
||||
path: string;
|
||||
@@ -40,6 +43,8 @@ export default function MatchGameModal({
|
||||
const [isSearching, setIsSearching] = useState(false);
|
||||
const [isMatching, setIsMatching] = useState<string | null>(null);
|
||||
|
||||
const state = useSnapshot(pluginState).state;
|
||||
|
||||
useEffect(() => {
|
||||
setSearchTerm(initialSearchTerm);
|
||||
setSearchResults([]);
|
||||
@@ -51,7 +56,7 @@ export default function MatchGameModal({
|
||||
|
||||
async function search() {
|
||||
setIsSearching(true);
|
||||
const results = await GameEndpoint.getPotentialMatches(searchTerm, true);
|
||||
const results = await GameEndpoint.getPotentialMatches(searchTerm);
|
||||
setSearchResults(results);
|
||||
setIsSearching(false);
|
||||
}
|
||||
@@ -86,7 +91,7 @@ export default function MatchGameModal({
|
||||
<div>
|
||||
<Table removeWrapper isStriped isHeaderSticky
|
||||
classNames={{
|
||||
base: "h-80 overflow-scroll",
|
||||
base: "h-80 overflow-y-auto",
|
||||
}}
|
||||
>
|
||||
<TableHeader>
|
||||
@@ -120,7 +125,8 @@ export default function MatchGameModal({
|
||||
<TableCell>
|
||||
<div className="flex flex-row gap-2">
|
||||
{Object.values(item.originalIds).map(
|
||||
originalId => <PluginIcon pluginId={originalId.pluginId}/>
|
||||
originalId => <PluginIcon
|
||||
plugin={state[originalId.pluginId] as PluginDto}/>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
import {Image, Tooltip} from "@heroui/react";
|
||||
import {Plug} from "@phosphor-icons/react";
|
||||
import {pluginState} from "Frontend/state/PluginState";
|
||||
import {useSnapshot} from "valtio/react";
|
||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||
|
||||
interface PluginLogoProps {
|
||||
pluginId: string;
|
||||
interface PluginIconProps {
|
||||
plugin: PluginDto;
|
||||
size?: number;
|
||||
blurred?: boolean;
|
||||
showTooltip?: boolean;
|
||||
}
|
||||
|
||||
export default function PluginIcon({pluginId}: PluginLogoProps) {
|
||||
const state = useSnapshot(pluginState);
|
||||
export default function PluginIcon({
|
||||
plugin,
|
||||
size = 16,
|
||||
blurred = false,
|
||||
showTooltip = true
|
||||
}: PluginIconProps) {
|
||||
|
||||
return state.isLoaded && (
|
||||
<Tooltip content={state.state[pluginId].name}>
|
||||
{state.state[pluginId].hasLogo ?
|
||||
<Image src={`/images/plugins/${state.state[pluginId].id}/logo`} width={16} height={16} radius="none"/> :
|
||||
<Plug size={16} weight="fill"/>
|
||||
}
|
||||
</Tooltip>
|
||||
)
|
||||
const icon = plugin.hasLogo
|
||||
?
|
||||
<Image isBlurred={blurred} src={`/images/plugins/${plugin.id}/logo`} width={size} height={size} radius="none"/>
|
||||
: <Plug size={size} weight="fill"/>;
|
||||
|
||||
return showTooltip
|
||||
? <Tooltip content={plugin.name}>{icon}</Tooltip>
|
||||
: icon;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import {Plug} from "@phosphor-icons/react";
|
||||
import React from "react";
|
||||
import {Image} from "@heroui/react";
|
||||
import PluginIcon from "Frontend/components/general/plugin/PluginIcon";
|
||||
import PluginDto from "Frontend/generated/org/gameyfin/app/core/plugins/dto/PluginDto";
|
||||
|
||||
interface PluginLogoProps {
|
||||
@@ -8,12 +7,5 @@ interface PluginLogoProps {
|
||||
}
|
||||
|
||||
export default function PluginLogo({plugin}: PluginLogoProps) {
|
||||
return (
|
||||
<>
|
||||
{plugin.hasLogo ?
|
||||
<Image isBlurred src={`/images/plugins/${plugin.id}/logo`} width={64} height={64} radius="none"/> :
|
||||
<Plug size={64} weight="fill"/>
|
||||
}
|
||||
</>
|
||||
);
|
||||
return <PluginIcon plugin={plugin} size={64} blurred={true} showTooltip={false}/>
|
||||
}
|
||||
@@ -11,7 +11,7 @@ interface PluginManagementSectionProps {
|
||||
plugins: PluginDto[];
|
||||
}
|
||||
|
||||
export function PluginManagementSection({type, plugins}: PluginManagementSectionProps) {
|
||||
export function PluginManagementSection({type, plugins = []}: PluginManagementSectionProps) {
|
||||
const pluginPrioritiesModal = useDisclosure();
|
||||
|
||||
return (
|
||||
@@ -20,17 +20,24 @@ export function PluginManagementSection({type, plugins}: PluginManagementSection
|
||||
<h2 className="text-xl font-bold">{camelCaseToTitle(type)}</h2>
|
||||
|
||||
<Tooltip color="foreground" placement="left" content="Change plugin order">
|
||||
<Button isIconOnly variant="flat" onPress={pluginPrioritiesModal.onOpen}>
|
||||
<Button isIconOnly
|
||||
variant="flat"
|
||||
onPress={pluginPrioritiesModal.onOpen}
|
||||
isDisabled={plugins.length === 0}>
|
||||
<ListNumbers/>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-300px gap-4">
|
||||
{plugins.length === 0 && <div className="flex flex-row justify-center">
|
||||
<p className="text-gray-500">No plugins of this type installed.</p>
|
||||
</div>}
|
||||
|
||||
{plugins.length > 0 && <div className="grid grid-cols-300px gap-4">
|
||||
{plugins.map((plugin) =>
|
||||
<PluginManagementCard plugin={plugin} key={plugin.id}/>
|
||||
)}
|
||||
</div>
|
||||
</div>}
|
||||
|
||||
<PluginPrioritiesModal
|
||||
key={plugins.map(p => p.id + p.priority).join(',')} // force re-mount if plugin order changes
|
||||
|
||||
@@ -79,13 +79,21 @@ export default function GameView() {
|
||||
return game && (
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="overflow-hidden relative rounded-t-lg">
|
||||
{(game.imageIds && game.imageIds.length > 0) ?
|
||||
<img className="w-full h-96 object-cover brightness-50 blur-sm scale-110"
|
||||
alt="Game screenshot"
|
||||
src={`/images/screenshot/${game.imageIds[0]}`}
|
||||
/> :
|
||||
{game.headerId ? (
|
||||
<img
|
||||
className="w-full h-96 object-cover brightness-50 blur-sm scale-110"
|
||||
alt="Game header"
|
||||
src={`/images/header/${game.headerId}`}
|
||||
/>
|
||||
) : game.imageIds && game.imageIds.length > 0 ? (
|
||||
<img
|
||||
className="w-full h-96 object-cover brightness-50 blur-sm scale-110"
|
||||
alt="Game screenshot"
|
||||
src={`/images/screenshot/${game.imageIds[0]}`}
|
||||
/>
|
||||
) : (
|
||||
<div className="w-full h-96 bg-secondary relative"/>
|
||||
}
|
||||
)}
|
||||
<div className="absolute inset-0 bg-gradient-to-b from-transparent to-background"/>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 mx-24">
|
||||
|
||||
Reference in New Issue
Block a user