mirror of
https://github.com/BrenBroZAYT/gameyfin.git
synced 2026-06-13 16:40:01 +00:00
Almost finished HomeView
This commit is contained in:
@@ -60,7 +60,7 @@ export default function ProfileMenu() {
|
||||
</div>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu disabledKeys={["username"]}>
|
||||
<DropdownItem key="username">
|
||||
<DropdownItem key="username" textValue={auth.state.user?.username}>
|
||||
<p className="font-bold">Signed in as {auth.state.user?.username}</p>
|
||||
</DropdownItem>
|
||||
{profileMenuItems.filter(item => item.showIf !== false).map(({label, icon, onClick, color}) => {
|
||||
@@ -72,6 +72,7 @@ export default function ProfileMenu() {
|
||||
/* @ts-ignore */
|
||||
color={color ? color : ""}
|
||||
className={`text-${color} hover:bg-primary/20`}
|
||||
textValue={label}
|
||||
>
|
||||
{label}
|
||||
</DropdownItem>
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import React, {useEffect, useRef, useState} from "react";
|
||||
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {Card} from "@heroui/react";
|
||||
import {ArrowRight} from "@phosphor-icons/react";
|
||||
|
||||
interface CoverRowProps {
|
||||
games: GameDto[];
|
||||
title: string;
|
||||
onPressShowMore: () => void;
|
||||
}
|
||||
|
||||
const aspectRatio = 12 / 17; // aspect ratio of the game cover
|
||||
const defaultImageHeight = 300; // default height for the image
|
||||
const defaultImageWidth = aspectRatio * defaultImageHeight; // default width for the image
|
||||
const radius = "sm";
|
||||
|
||||
export function CoverRow({games, title, onPressShowMore}: CoverRowProps) {
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [visibleCount, setVisibleCount] = useState(games.length);
|
||||
|
||||
useEffect(() => {
|
||||
const calculateVisible = () => {
|
||||
if (containerRef.current) {
|
||||
const containerWidth = containerRef.current.offsetWidth;
|
||||
const maxFit = Math.floor((containerWidth - defaultImageWidth) / defaultImageWidth) + 1;
|
||||
setVisibleCount(maxFit < games.length ? maxFit : games.length);
|
||||
}
|
||||
};
|
||||
|
||||
const resizeObserver = new ResizeObserver(calculateVisible);
|
||||
if (containerRef.current) {
|
||||
resizeObserver.observe(containerRef.current);
|
||||
}
|
||||
|
||||
calculateVisible(); // initial calculation
|
||||
|
||||
return () => resizeObserver.disconnect();
|
||||
}, [games.length]);
|
||||
|
||||
const showMore = visibleCount < games.length;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mb-4">
|
||||
<p className="text-2xl font-bold mb-4">{title}</p>
|
||||
<div className="w-full relative">
|
||||
<Card ref={containerRef} className="flex flex-row gap-4 bg-transparent" radius={radius}>
|
||||
{games.slice(0, visibleCount).map((game, index) => (
|
||||
<div className="flex-shrink-0" key={index}>
|
||||
<GameCover game={game} radius={radius}/>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
|
||||
{showMore && (
|
||||
<div className="flex flex-row items-center justify-end cursor-pointer"
|
||||
onClick={onPressShowMore}>
|
||||
<div className="absolute h-full w-1/4 right-0 bottom-0
|
||||
bg-gradient-to-r from-transparent to-background
|
||||
transition-all duration-300 ease-in-out hover:opacity-80"/>
|
||||
<div
|
||||
className="absolute h-full right-0 bottom-0 flex flex-row items-center gap-2 pointer-events-none">
|
||||
<p className="text-xl font-semibold">Show more</p>
|
||||
<ArrowRight weight="bold"/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {Image} from "@heroui/react";
|
||||
|
||||
interface GameCoverProps {
|
||||
game: GameDto;
|
||||
size?: number;
|
||||
radius?: "none" | "sm" | "md" | "lg" | "full";
|
||||
}
|
||||
|
||||
export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) {
|
||||
return (
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 w-full h-full object-cover aspect-[12/17]"
|
||||
src={`images/cover/${game.coverId}`}
|
||||
radius={radius}
|
||||
height={size}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import Section from "Frontend/components/general/Section";
|
||||
import {GameCover} from "Frontend/components/general/GameCover";
|
||||
import {Card} from "@heroui/react";
|
||||
|
||||
interface HorizontalGameListProps {
|
||||
title: string;
|
||||
games: GameDto[];
|
||||
}
|
||||
|
||||
export function HorizontalGameList({title, games}: HorizontalGameListProps) {
|
||||
return (
|
||||
<div className="flex flex-col gap-2">
|
||||
<Section title={title}/>
|
||||
<div className="flex flex-row gap-4 overflow-x-auto">
|
||||
{games.length > 0 ?
|
||||
games.map((game) => (
|
||||
<GameCover game={game}/>
|
||||
))
|
||||
: <Card className="h-[300px] aspect-[12/17]">
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
<p className="text-gray-500">No content</p>
|
||||
</div>
|
||||
</Card>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import {GameCover} from "Frontend/components/general/GameCover";
|
||||
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
|
||||
export function GameOverviewCard({game}: { game: GameDto }) {
|
||||
|
||||
@@ -3,7 +3,7 @@ import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/Libr
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import React, {useEffect, useState} from "react";
|
||||
import {LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import {GameCover} from "Frontend/components/general/GameCover";
|
||||
import {GameCover} from "Frontend/components/general/covers/GameCover";
|
||||
import {
|
||||
Alien,
|
||||
CastleTurret,
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {Image} from "@heroui/react";
|
||||
import {GameCoverFallback} from "Frontend/components/general/covers/GameCoverFallback";
|
||||
|
||||
interface GameCoverProps {
|
||||
game: GameDto;
|
||||
size?: number;
|
||||
radius?: "none" | "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export function GameCover({game, size = 300, radius = "sm"}: GameCoverProps) {
|
||||
return (
|
||||
Number.isInteger(game.coverId) ? (
|
||||
<Image
|
||||
alt={game.title}
|
||||
className="z-0 w-full h-full object-cover aspect-[12/17]"
|
||||
src={`images/cover/${game.coverId}`}
|
||||
radius={radius}
|
||||
height={size}
|
||||
fallbackSrc={<GameCoverFallback title={game.title} size={size} radius={radius}/>}
|
||||
/>
|
||||
) : <GameCoverFallback title={game.title} size={size} radius={radius}/>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import {Card} from "@heroui/react";
|
||||
|
||||
interface GameCoverFallbackProps {
|
||||
title: string;
|
||||
size?: number;
|
||||
radius?: "none" | "sm" | "md" | "lg";
|
||||
}
|
||||
|
||||
export function GameCoverFallback({title, size = 300, radius = "sm"}: GameCoverFallbackProps) {
|
||||
return (
|
||||
<Card style={{aspectRatio: "12 /17", height: size, borderRadius: radius}}
|
||||
radius={radius}>
|
||||
<div className="flex flex-col items-center justify-center h-full">
|
||||
{title}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -87,11 +87,11 @@ export function timeUntil(instantString: string, timeZone: string = moment.tz.gu
|
||||
* @param count
|
||||
* @returns {GameDto[]}
|
||||
*/
|
||||
export async function randomGamesFromLibrary(library: LibraryDto, count: number): Promise<GameDto[]> {
|
||||
export async function randomGamesFromLibrary(library: LibraryDto, count?: number): Promise<GameDto[]> {
|
||||
const rand = new Rand(library.id.toString());
|
||||
const games = await LibraryEndpoint.getGamesInLibrary(library.id);
|
||||
return games
|
||||
.sort((a: GameDto, b: GameDto) => a.id - b.id)
|
||||
.sort(() => rand.next() - 0.5)
|
||||
.slice(0, count);
|
||||
.slice(0, count ?? games.length);
|
||||
}
|
||||
@@ -1,9 +1,9 @@
|
||||
import {useEffect, useState} from "react";
|
||||
import {GameEndpoint, LibraryEndpoint} from "Frontend/generated/endpoints";
|
||||
import LibraryDto from "Frontend/generated/de/grimsi/gameyfin/libraries/dto/LibraryDto";
|
||||
import {HorizontalGameList} from "Frontend/components/general/HorizontalGameList";
|
||||
import GameDto from "Frontend/generated/de/grimsi/gameyfin/games/dto/GameDto";
|
||||
import {randomGamesFromLibrary} from "Frontend/util/utils";
|
||||
import {CoverRow} from "Frontend/components/general/CoverRow";
|
||||
|
||||
export default function HomeView() {
|
||||
const [recentlyAddedGames, setRecentlyAddedGames] = useState<GameDto[]>([]);
|
||||
@@ -15,7 +15,7 @@ export default function HomeView() {
|
||||
setLibraries(libraries);
|
||||
|
||||
const gamePromises = libraries.map((library) =>
|
||||
randomGamesFromLibrary(library, 10).then((games) => [library.id, games] as [number, GameDto[]])
|
||||
randomGamesFromLibrary(library).then((games) => [library.id, games] as [number, GameDto[]])
|
||||
);
|
||||
|
||||
Promise.all(gamePromises).then((results) => {
|
||||
@@ -35,12 +35,14 @@ export default function HomeView() {
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<p className="text-center text-2xl font-extrabold">Welcome to Gameyfin!</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
<HorizontalGameList title="Recently added" games={recentlyAddedGames}/>
|
||||
<CoverRow title="Recently added" games={recentlyAddedGames}
|
||||
onPressShowMore={() => alert("show more of 'Recently added'")}/>
|
||||
{libraries.map((library) => (
|
||||
<HorizontalGameList key={library.id} title={library.name}
|
||||
games={libraryIdToGames.get(library.id) || []}/>
|
||||
<CoverRow key={library.id} title={library.name}
|
||||
games={libraryIdToGames.get(library.id) || []}
|
||||
onPressShowMore={() => alert(`show more of library '${library.name}'`)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -57,35 +57,34 @@ export default function MainLayout() {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col min-h-svh">
|
||||
<div className="flex flex-col min-h-screen">
|
||||
{isExploding ? <Confetti {...confettiProps}/> : <></>}
|
||||
<div className="flex flex-col flex-grow w-full 2xl:w-3/4 m-auto">
|
||||
<Navbar maxWidth="full">
|
||||
<NavbarBrand as="button" onClick={() => navigate('/')}>
|
||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
{auth.state.user?.emailConfirmed === false ?
|
||||
<NavbarItem>
|
||||
<small className="text-warning">Please confirm your email</small>
|
||||
</NavbarItem>
|
||||
:
|
||||
""
|
||||
}
|
||||
<NavbarItem>
|
||||
<ProfileMenu/>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
|
||||
<div className="w-full overflow-hidden ml-2 pr-8 mt-4">
|
||||
<Outlet/>
|
||||
</div>
|
||||
<Navbar maxWidth="full" className="2xl:px-[12.5%]">
|
||||
<NavbarBrand as="button" onClick={() => navigate('/')}>
|
||||
<GameyfinLogo className="h-10 fill-foreground"/>
|
||||
</NavbarBrand>
|
||||
<NavbarContent justify="end">
|
||||
{auth.state.user?.emailConfirmed === false ?
|
||||
<NavbarItem>
|
||||
<small className="text-warning">Please confirm your email</small>
|
||||
</NavbarItem>
|
||||
:
|
||||
""
|
||||
}
|
||||
<NavbarItem>
|
||||
<ProfileMenu/>
|
||||
</NavbarItem>
|
||||
</NavbarContent>
|
||||
</Navbar>
|
||||
|
||||
<div className="flex flex-col flex-grow 2xl:px-[12.5%] overflow-x-hidden mt-4">
|
||||
<Outlet/>
|
||||
</div>
|
||||
|
||||
<Divider className="mt-8"/>
|
||||
<div className="flex flex-col w-full 2xl:w-3/4 m-auto">
|
||||
<footer className="flex flex-row items-center justify-between py-4 px-12">
|
||||
<div className="flex flex-col w-full 2xl:px-[12.5%]">
|
||||
<footer className="flex flex-row items-center justify-between py-4">
|
||||
<p>Gameyfin {PackageJson.version}</p>
|
||||
<p className="flex flex-row gap-1 items-baseline">
|
||||
Made with
|
||||
|
||||
Reference in New Issue
Block a user