Skip to content

Commit

Permalink
feat : add mods galleries
Browse files Browse the repository at this point in the history
  • Loading branch information
Juknum committed Aug 10, 2024
1 parent 3c18982 commit 918d2d2
Show file tree
Hide file tree
Showing 7 changed files with 385 additions and 56 deletions.
209 changes: 189 additions & 20 deletions src/app/(pages)/mods/[modId]/gallery/page.tsx
Original file line number Diff line number Diff line change
@@ -1,46 +1,215 @@
import { Group, Select, Stack, TextInput } from '@mantine/core';
'use client';

import { useParams } from 'next/navigation';

import { useEffect, useMemo, useRef, useState, useTransition } from 'react';

import { CloseButton, Group, Loader, Pagination, Select, Stack, Text, TextInput } from '@mantine/core';
import { usePrevious } from '@mantine/hooks';
import { Resolution } from '@prisma/client';

import { useDeviceSize } from '~/hooks/use-device-size';
import { useEffectOnce } from '~/hooks/use-effect-once';
import { BREAKPOINT_MOBILE_LARGE, BREAKPOINT_TABLET, ITEMS_PER_PAGE, ITEMS_PER_ROW } from '~/lib/constants';
import { getLatestContributionsOfModVersion } from '~/server/data/contributions';
import { getModVersionFromMod } from '~/server/data/mods-version';
import { getTexturesFromModVersion } from '~/server/data/texture';

import { GalleryTexture } from './texture';

import type { ModVersion, Texture } from '@prisma/client';
import type { ContributionWithCoAuthors } from '~/types';

export default function ModGalleryPage() {
const [resolution, setResolution] = useState<Resolution | 'x16'>(Resolution['x32']);
const [isLoading, startTransition] = useTransition();

const modId = useParams().modId! as string;
const [modVersions, setModVersions] = useState<ModVersion[]>([]);
const [modVersionShown, setModVersionShown] = useState<string | null>(null);

const [textures, setTextures] = useState<Texture[]>([]);
const [texturesFiltered, setTexturesFiltered] = useState<Texture[]>([]);
const [texturesShown, setTexturesShown] = useState<Texture[][]>([[]]);

const [contributions, setContributions] = useState<ContributionWithCoAuthors[]>([]);

const [texturesShownPerPage, setTexturesShownPerPage] = useState<string>('96');
const [texturesShownPerRow, setTexturesShownPerRow] = useState<number>(12);

const itemsPerRow = useMemo(() => ITEMS_PER_ROW, []);
const itemsPerPage = useMemo(() => [...ITEMS_PER_PAGE, '192', '384', 'All'], []);

const [activePage, setActivePage] = useState(1);
const [search, setSearch] = useState('');
const prevSearchedTextures = usePrevious(texturesFiltered);

const [windowWidth] = useDeviceSize();
const texturesGroupRef = useRef<HTMLDivElement>(null);

useEffectOnce(() => {
getModVersionFromMod(modId).then((versions) => {
setModVersions(versions);
setModVersionShown(versions[0]?.id ?? null);
});
});

useEffect(() => {
startTransition(() => {
const chunks: Texture[][] = [];
const int = texturesShownPerPage === 'All' ? texturesFiltered.length : parseInt(texturesShownPerPage ?? itemsPerPage[0]);

for (let i = 0; i < texturesFiltered.length; i += int) {
chunks.push(texturesFiltered.slice(i, i + int));
}

if (!prevSearchedTextures || prevSearchedTextures.length !== texturesFiltered.length) {
setActivePage(1);
}

setTexturesShown(chunks);
});
}, [texturesFiltered, itemsPerPage, prevSearchedTextures, texturesShownPerPage]);

useEffect(() => {
startTransition(() => {
if (!search) {
setTexturesFiltered(textures);
return;
}

setTexturesFiltered(
textures.filter((texture) =>
texture.name.toLowerCase().includes(search.toLowerCase())
|| texture.aliases.some((alias) => alias.toLowerCase().includes(search.toLowerCase()))
)
);
});
}, [textures, search]);

useEffect(() => {
if (!modVersionShown) return; // should not happens but just for TS

startTransition(() => {
getTexturesFromModVersion(modVersionShown)
.then(setTextures);

if (resolution !== 'x16')
getLatestContributionsOfModVersion(modVersionShown, resolution)
.then(setContributions);
});
}, [modVersionShown, resolution]);

return (
<Stack gap="sm">
<Group gap="sm" wrap="nowrap">
<Group w="100%" gap="sm" wrap="nowrap">
<Stack gap="sm" mb="sm" ref={texturesGroupRef} maw="1384">
<Group gap="sm" wrap={windowWidth <= BREAKPOINT_TABLET ? 'wrap' : 'nowrap'}>
<Group
w={windowWidth <= BREAKPOINT_TABLET ? '100%' : '30%'}
gap="sm"
wrap="nowrap"
>
<Select
label="Resolution"
// data={itemsPerPage}
// value={versionShownPerPage}
// onChange={(e) => e ? setVersionsShownPerPage(e) : null}
data={Object.entries(Object.assign({ x16: 'x16' }, Resolution)).map(([k, v]) => ({ value: k, label: v }))}
value={resolution}
onChange={(e) => e ? setResolution(e as Resolution) : null}
checkIconPosition="right"
w={120}
/>
<Select
label="Mod version"
w="100%"
maw={'calc(100% - 120px - var(--mantine-spacing-sm))'}
// data={itemsPerPage}
// value={versionShownPerPage}
// onChange={(e) => e ? setVersionsShownPerPage(e) : null}
data={modVersions.map((v) => ({ value: v.id, label: v.version }))}
value={modVersionShown}
onChange={(e) => e ? setModVersionShown(e) : null}
checkIconPosition="right"
searchable
/>
</Group>
<Group w="100%" gap="sm" wrap="nowrap">
<Group
w={windowWidth <= BREAKPOINT_TABLET ? '100%' : '70%'}
gap="sm"
wrap={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'wrap' : 'nowrap'}
>
<TextInput
w="100%"
maw={'calc(100% - 120px - var(--mantine-spacing-sm))'}
maw={windowWidth > BREAKPOINT_MOBILE_LARGE ? 'calc(100% - 240px - var(--mantine-spacing-sm))' : undefined}
label="Search"
// value={search}
// onChange={(e) => setSearch(e.currentTarget.value)}
value={search}
onChange={(e) => setSearch(e.currentTarget.value)}
placeholder="Search for a texture name..."
rightSection={search && (
<CloseButton
style={{ '--cb-size': 'var(--cb-size-sm)' }}
onClick={() => setSearch('')}
/>
)}
/>
<Select
label="Results per page"
// data={itemsPerPage}
// value={versionShownPerPage}
// onChange={(e) => e ? setVersionsShownPerPage(e) : null}
withCheckIcon={false}
w={120}
label="Textures per page"
data={itemsPerPage}
value={texturesShownPerPage}
onChange={(e) => e ? setTexturesShownPerPage(e) : null}
checkIconPosition="right"
w={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'calc(50% - var(--mantine-spacing-sm) / 2)' : 120}
/>
<Select
label="Textures per row"
data={itemsPerRow}
value={texturesShownPerRow.toString()}
onChange={(e) => e ? setTexturesShownPerRow(parseInt(e)) : null}
checkIconPosition="right"
w={windowWidth <= BREAKPOINT_MOBILE_LARGE ? 'calc(50% - var(--mantine-spacing-sm) / 2)' : 120}
/>
</Group>
</Group>

{isLoading && (
<Group
align="center"
justify="center"
h="100px"
gap="md"
style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }}
>
<Loader color="blue" mt={5} />
</Group>
)}

{!isLoading && texturesFiltered.length === 0 && (
<Group
align="center"
justify="center"
h="100px"
gap="md"
style={{ height: 'calc(81% - (2 * var(--mantine-spacing-sm) - 62px))' }}
>
<Text c="dimmed">No results for &quot;{search}&quot;</Text>
</Group>
)}

{!isLoading && texturesShown[activePage - 1]?.length && (
<>
<Group gap={10} h="100%">
{texturesShown[activePage - 1]?.map((texture) => (
<GalleryTexture
key={texture.id}
container={texturesGroupRef}
rowItemsGap={10}
rowItemsLength={texturesShownPerRow}
resolution={resolution}
texture={texture}
contribution={contributions.find((c) => c.textureId === texture.id)}
/>
))}
</Group>

<Group w="100%" justify="center">
<Pagination total={texturesShown.length} value={activePage} onChange={setActivePage} />
</Group>
</>
)}
</Stack>
);
}
133 changes: 133 additions & 0 deletions src/app/(pages)/mods/[modId]/gallery/texture.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { useMemo } from 'react';
import type { RefObject } from 'react';

import { GoFileDiff, GoHash, GoPeople } from 'react-icons/go';
import { PiApproximateEquals } from 'react-icons/pi';

import { Avatar, Group, Stack, Text } from '@mantine/core';

import { TextureImage } from '~/components/texture-img';
import { Tile } from '~/components/tile';

import type { CardProps, PolymorphicComponentProps } from '@mantine/core';
import type { Resolution, Texture } from '@prisma/client';
import type { ContributionWithCoAuthors } from '~/types';

export interface GalleryTextureProps {
container: RefObject<HTMLDivElement>;
rowItemsGap: number;
rowItemsLength: number;
texture: Texture;
resolution: Resolution | 'x16';
contribution?: ContributionWithCoAuthors;
}

function SmallTile({ children, style, ...props }: PolymorphicComponentProps<'div', CardProps>) {
return (
<Tile
w="100%"
mih={28}
style={{
padding: '5px 8px 6px 8px',
borderRadius: 5,
...style,
}}
{...props}
>
{children}
</Tile>
);
}

export function GalleryTexture({
container,
rowItemsGap,
rowItemsLength,
resolution,
texture,
contribution,
}: GalleryTextureProps) {

const src = useMemo(() => {
if (!contribution || resolution === 'x16') return texture.filepath;
return contribution.filepath;
}, [texture, resolution, contribution]);

const mcmeta = useMemo(() => {
if (!contribution || resolution === 'x16') return texture.mcmeta;
return contribution.mcmeta;
}, [texture, resolution, contribution]);

const size = useMemo(() => ((container.current?.clientWidth ?? 1) - (rowItemsGap * (rowItemsLength - 1))) / rowItemsLength,
[container, rowItemsGap, rowItemsLength]
);

return (
<TextureImage
alt={texture.name}
src={src}
mcmeta={mcmeta}
isTransparent={resolution !== 'x16' && !contribution}
size={size}
popupStyles={{
backgroundColor: 'transparent',
padding: 0,
border: 'none',
boxShadow: 'none',
}}
>
<Stack gap={2} align="start" miw={400} maw={400}>
<SmallTile>
<Text fw={500} ta="center">{texture.name}</Text>
</SmallTile>
{resolution !== 'x16' && contribution && (
<Group gap={2} w="100%" wrap="nowrap" align="start">
<SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}>
<GoFileDiff />
</SmallTile>
<SmallTile>
<Group gap={3.3}>
<Avatar mt={1} src={contribution.owner.image} size="xs" mr={3} />
<Text component="span" size="xs">{contribution.owner.name}</Text>
</Group>
</SmallTile>
</Group>
)}
{resolution !== 'x16' && contribution && contribution.coAuthors.length > 0 && (
<Group gap={2} w="100%" wrap="nowrap" align="start">
<SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}>
<GoPeople />
</SmallTile>
<SmallTile>
<Text size="xs">
{contribution.coAuthors.map((ca) => ca.name).join(', ')}
</Text>
</SmallTile>
</Group>
)}
<Group gap={2} w="100%" wrap="nowrap" align="start">
<SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}>
<GoHash />
</SmallTile>
<SmallTile>
<Text size="xs" c="dimmed">
ID: {texture.id}
</Text>
</SmallTile>
</Group>
{texture.aliases.length > 0 && (
<Group gap={2} w="100%" wrap="nowrap" align="start">
<SmallTile className="navbar-icon-fix" style={{ '--size': '28px' }}>
<PiApproximateEquals />
</SmallTile>
<SmallTile>
<Text size="xs" c="dimmed">
{texture.aliases.join(', ')}
</Text>
</SmallTile>
</Group>
)}
</Stack>
</TextureImage>
);
}
Loading

0 comments on commit 918d2d2

Please sign in to comment.