-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
385 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 "{search}"</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> | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> | ||
); | ||
} |
Oops, something went wrong.