mirror of
https://github.com/captbaritone/webamp.git
synced 2026-01-23 02:15:01 +00:00
More stuff for the new layout
This commit is contained in:
parent
c778464c42
commit
8efe121f3c
11 changed files with 534 additions and 76 deletions
|
|
@ -13,6 +13,7 @@ import {
|
|||
import Link from "next/link";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { MOBILE_MAX_WIDTH } from "../../../legacy-client/src/constants";
|
||||
|
||||
export default function BottomMenuBar() {
|
||||
const [isHamburgerOpen, setIsHamburgerOpen] = useState(false);
|
||||
|
|
@ -49,52 +50,55 @@ export default function BottomMenuBar() {
|
|||
{/* Hamburger Menu Overlay */}
|
||||
{isHamburgerOpen && (
|
||||
<div
|
||||
ref={menuRef}
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: "4.5rem",
|
||||
left: 0,
|
||||
left: "50%",
|
||||
transform: "translateX(-50%)",
|
||||
width: "100%",
|
||||
maxWidth: MOBILE_MAX_WIDTH,
|
||||
backgroundColor: "rgba(26, 26, 26, 0.98)",
|
||||
backdropFilter: "blur(10px)",
|
||||
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
zIndex: 999,
|
||||
}}
|
||||
>
|
||||
<HamburgerMenuItem
|
||||
href="/about"
|
||||
icon={<Info size={20} />}
|
||||
label="About"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
/>
|
||||
<HamburgerMenuItem
|
||||
href="/upload"
|
||||
icon={<Upload size={20} />}
|
||||
label="Upload"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
/>{" "}
|
||||
<HamburgerMenuItem
|
||||
href="https://github.com/captbaritone/webamp/issues"
|
||||
icon={<MessageSquare size={20} />}
|
||||
label="Feedback"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
external
|
||||
/>
|
||||
<HamburgerMenuItem
|
||||
href="https://github.com/captbaritone/webamp/"
|
||||
icon={<Github size={20} />}
|
||||
label="GitHub"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
external
|
||||
/>
|
||||
<div ref={menuRef}>
|
||||
<HamburgerMenuItem
|
||||
href="/about"
|
||||
icon={<Info size={20} />}
|
||||
label="About"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
/>
|
||||
<HamburgerMenuItem
|
||||
href="/upload"
|
||||
icon={<Upload size={20} />}
|
||||
label="Upload"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
/>{" "}
|
||||
<HamburgerMenuItem
|
||||
href="https://github.com/captbaritone/webamp/issues"
|
||||
icon={<MessageSquare size={20} />}
|
||||
label="Feedback"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
external
|
||||
/>
|
||||
<HamburgerMenuItem
|
||||
href="https://github.com/captbaritone/webamp/"
|
||||
icon={<Github size={20} />}
|
||||
label="GitHub"
|
||||
onClick={() => {
|
||||
setIsHamburgerOpen(false);
|
||||
}}
|
||||
external
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
@ -110,36 +114,48 @@ export default function BottomMenuBar() {
|
|||
borderTop: "1px solid rgba(255, 255, 255, 0.1)",
|
||||
padding: "0.75rem 0",
|
||||
display: "flex",
|
||||
justifyContent: "space-evenly",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
zIndex: 1000,
|
||||
}}
|
||||
>
|
||||
<MenuButton
|
||||
href="/scroll"
|
||||
icon={<Smartphone size={24} />}
|
||||
label="Feed"
|
||||
isActive={pathname === "/scroll"}
|
||||
/>
|
||||
<MenuButton
|
||||
href="/"
|
||||
icon={<Grid3x3 size={24} />}
|
||||
label="Grid"
|
||||
isActive={pathname === "/"}
|
||||
/>
|
||||
<MenuButton
|
||||
href="/"
|
||||
icon={<Search size={24} />}
|
||||
label="Search"
|
||||
isActive={false}
|
||||
/>
|
||||
<MenuButton
|
||||
icon={<Menu size={24} />}
|
||||
label="Menu"
|
||||
onClick={toggleHamburger}
|
||||
isButton
|
||||
isActive={false}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
width: "100%",
|
||||
maxWidth: MOBILE_MAX_WIDTH, // Match the scroll page max width
|
||||
display: "flex",
|
||||
justifyContent: "space-evenly",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<MenuButton
|
||||
href="/scroll"
|
||||
icon={<Smartphone size={24} />}
|
||||
label="Feed"
|
||||
isActive={
|
||||
pathname === "/scroll" || pathname.startsWith("/scroll/skin")
|
||||
}
|
||||
/>
|
||||
<MenuButton
|
||||
href="/scroll/grid"
|
||||
icon={<Grid3x3 size={24} />}
|
||||
label="Grid"
|
||||
isActive={pathname === "/scroll/grid"}
|
||||
/>
|
||||
<MenuButton
|
||||
href="/"
|
||||
icon={<Search size={24} />}
|
||||
label="Search"
|
||||
isActive={false}
|
||||
/>
|
||||
<MenuButton
|
||||
icon={<Menu size={24} />}
|
||||
label="Menu"
|
||||
onClick={toggleHamburger}
|
||||
isButton
|
||||
isActive={false}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
|
@ -151,6 +167,7 @@ type MenuButtonProps = {
|
|||
label: string;
|
||||
isButton?: boolean;
|
||||
isActive?: boolean;
|
||||
onClick?: () => void;
|
||||
};
|
||||
|
||||
function MenuButton({
|
||||
|
|
@ -159,6 +176,7 @@ function MenuButton({
|
|||
label,
|
||||
isButton = false,
|
||||
isActive = false,
|
||||
onClick,
|
||||
}: MenuButtonProps) {
|
||||
const touchTargetSize = "3.0rem";
|
||||
|
||||
|
|
@ -230,6 +248,7 @@ function MenuButton({
|
|||
style={containerStyle}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={onClick}
|
||||
>
|
||||
{content}
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { unstable_ViewTransition as ViewTransition } from "react";
|
||||
import { ClientSkin } from "./SkinScroller";
|
||||
import SkinActionIcons from "./SkinActionIcons";
|
||||
|
||||
|
|
@ -31,15 +32,17 @@ export default function SkinPage({ skin, index, sessionId }: Props) {
|
|||
}}
|
||||
>
|
||||
<div style={{ position: "relative", flexShrink: 0 }}>
|
||||
<img
|
||||
src={skin.screenshotUrl}
|
||||
alt={skin.fileName}
|
||||
style={{
|
||||
width: "100%",
|
||||
aspectRatio: "275 / 348",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
/>
|
||||
<ViewTransition name={`skin-${skin.md5}`}>
|
||||
<img
|
||||
src={skin.screenshotUrl}
|
||||
alt={skin.fileName}
|
||||
style={{
|
||||
width: "100%",
|
||||
aspectRatio: "275 / 348",
|
||||
imageRendering: "pixelated",
|
||||
}}
|
||||
/>
|
||||
</ViewTransition>
|
||||
|
||||
<SkinActionIcons skin={skin} sessionId={sessionId} />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { useState, useLayoutEffect, useEffect } from "react";
|
|||
import SkinPage from "./SkinPage";
|
||||
import { logUserEvent } from "./Events";
|
||||
import { useScrollHint } from "./useScrollHint";
|
||||
import { MOBILE_MAX_WIDTH } from "../../../legacy-client/src/constants";
|
||||
|
||||
export type ClientSkin = {
|
||||
screenshotUrl: string;
|
||||
|
|
@ -158,8 +159,10 @@ export default function SkinScroller({
|
|||
<div
|
||||
ref={setContainerRef}
|
||||
style={{
|
||||
maxWidth: MOBILE_MAX_WIDTH, // 9:16 aspect ratio for scroll, full width for grid
|
||||
margin: "0 auto",
|
||||
height: "100vh",
|
||||
width: "100%",
|
||||
// width: "100%",
|
||||
overflowY: "scroll",
|
||||
scrollSnapType: "y mandatory",
|
||||
scrollbarWidth: "none", // Firefox
|
||||
|
|
|
|||
186
packages/skin-database/app/(modern)/scroll/grid/Grid.tsx
Normal file
186
packages/skin-database/app/(modern)/scroll/grid/Grid.tsx
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
"use client";
|
||||
import React, {
|
||||
useState,
|
||||
useMemo,
|
||||
useCallback,
|
||||
useRef,
|
||||
unstable_ViewTransition as ViewTransition,
|
||||
} from "react";
|
||||
|
||||
import Link from "next/link";
|
||||
import { FixedSizeGrid as Grid } from "react-window";
|
||||
import { useWindowSize } from "../../../../legacy-client/src/hooks";
|
||||
import {
|
||||
SCREENSHOT_WIDTH,
|
||||
SKIN_RATIO,
|
||||
} from "../../../../legacy-client/src/constants";
|
||||
import { getMuseumPageSkins, GridSkin } from "./getMuseumPageSkins";
|
||||
|
||||
type CellData = {
|
||||
skins: GridSkin[];
|
||||
columnCount: number;
|
||||
width: number;
|
||||
height: number;
|
||||
loadMoreSkins: (startIndex: number) => Promise<void>;
|
||||
};
|
||||
|
||||
function Cell({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
style,
|
||||
data,
|
||||
}: {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
style: React.CSSProperties;
|
||||
data: CellData;
|
||||
}) {
|
||||
const { skins, width, height, columnCount } = data;
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
data.loadMoreSkins(index);
|
||||
const skin = skins[index];
|
||||
|
||||
if (!skin) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style}>
|
||||
<div style={{ width, height, position: "relative" }}>
|
||||
<Link href={`/scroll/skin/${skin.md5}`}>
|
||||
<ViewTransition name={`skin-${skin.md5}`}>
|
||||
<img
|
||||
src={skin.screenshotUrl}
|
||||
alt={skin.fileName}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</ViewTransition>
|
||||
</Link>
|
||||
{skin.nsfw && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
background: "rgba(0,0,0,0.7)",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
color: "white",
|
||||
fontSize: "14px",
|
||||
}}
|
||||
>
|
||||
NSFW
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
type SkinTableProps = {
|
||||
initialSkins: GridSkin[];
|
||||
initialTotal: number;
|
||||
};
|
||||
|
||||
export default function SkinTable({
|
||||
initialSkins,
|
||||
initialTotal,
|
||||
}: SkinTableProps) {
|
||||
const { windowWidth, windowHeight } = useWindowSize();
|
||||
|
||||
// Initialize state with server-provided data
|
||||
const [skins, setSkins] = useState<GridSkin[]>(initialSkins);
|
||||
const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([0]));
|
||||
const isLoadingRef = useRef(false);
|
||||
|
||||
const columnCount = Math.round(windowWidth / (SCREENSHOT_WIDTH * 0.9));
|
||||
const columnWidth = windowWidth / columnCount;
|
||||
const rowHeight = columnWidth * SKIN_RATIO;
|
||||
const pageSize = 50; // Number of skins to load per page
|
||||
|
||||
const loadMoreSkins = useCallback(
|
||||
async (startIndex: number) => {
|
||||
const pageNumber = Math.floor(startIndex / pageSize);
|
||||
|
||||
// Don't reload if we already have this page
|
||||
if (loadedPages.has(pageNumber) || isLoadingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
isLoadingRef.current = true;
|
||||
try {
|
||||
const offset = pageNumber * pageSize;
|
||||
const newSkins = await getMuseumPageSkins(offset, pageSize);
|
||||
setSkins((prev) => [...prev, ...newSkins]);
|
||||
setLoadedPages((prev) => new Set([...prev, pageNumber]));
|
||||
} catch (error) {
|
||||
console.error("Failed to load skins:", error);
|
||||
} finally {
|
||||
isLoadingRef.current = false;
|
||||
}
|
||||
},
|
||||
[loadedPages, pageSize]
|
||||
);
|
||||
|
||||
function itemKey({
|
||||
columnIndex,
|
||||
rowIndex,
|
||||
}: {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
}) {
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
const skin = skins[index];
|
||||
return skin ? skin.md5 : `empty-cell-${columnIndex}-${rowIndex}`;
|
||||
}
|
||||
|
||||
const gridRef = React.useRef<any>();
|
||||
const itemRef = React.useRef<number>(0);
|
||||
|
||||
const onScroll = useMemo(() => {
|
||||
const half = Math.round(columnCount / 2);
|
||||
return (scrollData: { scrollTop: number }) => {
|
||||
itemRef.current =
|
||||
Math.round(scrollData.scrollTop / rowHeight) * columnCount + half;
|
||||
};
|
||||
}, [columnCount, rowHeight, loadMoreSkins]);
|
||||
|
||||
const itemData: CellData = useMemo(
|
||||
() => ({
|
||||
skins,
|
||||
columnCount,
|
||||
width: columnWidth,
|
||||
height: rowHeight,
|
||||
loadMoreSkins,
|
||||
}),
|
||||
[skins, columnCount, columnWidth, rowHeight, loadMoreSkins]
|
||||
);
|
||||
|
||||
return (
|
||||
<div id="infinite-skins">
|
||||
<Grid
|
||||
ref={gridRef}
|
||||
itemKey={itemKey}
|
||||
itemData={itemData}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
height={windowHeight}
|
||||
rowCount={Math.ceil(initialTotal / columnCount)}
|
||||
rowHeight={rowHeight}
|
||||
width={windowWidth}
|
||||
overscanRowsCount={5}
|
||||
onScroll={onScroll}
|
||||
style={{ overflowY: "scroll" }}
|
||||
>
|
||||
{Cell}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,198 @@
|
|||
"use client";
|
||||
|
||||
import {
|
||||
useState,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useRef,
|
||||
useCallback,
|
||||
memo,
|
||||
} from "react";
|
||||
import { FixedSizeGrid as Grid } from "react-window";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { ClientSkin } from "../SkinScroller";
|
||||
import {
|
||||
SCREENSHOT_WIDTH,
|
||||
SKIN_RATIO,
|
||||
} from "../../../../legacy-client/src/constants";
|
||||
|
||||
type Props = {
|
||||
initialSkins: ClientSkin[];
|
||||
getSkins: (sessionId: string, offset: number) => Promise<ClientSkin[]>;
|
||||
sessionId: string;
|
||||
};
|
||||
|
||||
type CellProps = {
|
||||
columnIndex: number;
|
||||
rowIndex: number;
|
||||
style: React.CSSProperties;
|
||||
data: {
|
||||
skins: ClientSkin[];
|
||||
columnCount: number;
|
||||
requestSkinsIfNeeded: (index: number) => void;
|
||||
};
|
||||
};
|
||||
|
||||
// Extract Cell as a separate component so we can use hooks
|
||||
const GridCell = memo(({ columnIndex, rowIndex, style, data }: CellProps) => {
|
||||
const { skins, columnCount, requestSkinsIfNeeded } = data;
|
||||
const router = useRouter();
|
||||
const index = rowIndex * columnCount + columnIndex;
|
||||
const skin = skins[index];
|
||||
|
||||
// Request more skins if this cell needs data
|
||||
useEffect(() => {
|
||||
if (!skin) {
|
||||
requestSkinsIfNeeded(index);
|
||||
}
|
||||
}, [skin, index, requestSkinsIfNeeded]);
|
||||
|
||||
if (!skin) {
|
||||
return <div style={style} />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
...style,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
backgroundColor: "#1a1a1a",
|
||||
padding: "2px",
|
||||
boxSizing: "border-box",
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
router.push(`/scroll/skin/${skin.md5}`);
|
||||
}}
|
||||
>
|
||||
<div style={{ width: "100%", height: "100%", position: "relative" }}>
|
||||
<img
|
||||
src={skin.screenshotUrl}
|
||||
alt={skin.fileName}
|
||||
style={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
display: "block",
|
||||
imageRendering: "pixelated",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
GridCell.displayName = "GridCell";
|
||||
|
||||
// Calculate grid dimensions based on window width
|
||||
// Skins will be scaled to fill horizontally across multiple columns
|
||||
function getGridDimensions(windowWidth: number) {
|
||||
const scale = 1.0; // Can be adjusted for different sizes
|
||||
const columnCount = Math.max(
|
||||
1,
|
||||
Math.floor(windowWidth / (SCREENSHOT_WIDTH * scale))
|
||||
);
|
||||
const columnWidth = windowWidth / columnCount;
|
||||
const rowHeight = columnWidth * SKIN_RATIO;
|
||||
return { columnWidth, rowHeight, columnCount };
|
||||
}
|
||||
|
||||
export default function InfiniteScrollGrid({
|
||||
initialSkins,
|
||||
getSkins,
|
||||
sessionId,
|
||||
}: Props) {
|
||||
const [skins, setSkins] = useState<ClientSkin[]>(initialSkins);
|
||||
const [fetching, setFetching] = useState(false);
|
||||
const [windowWidth, setWindowWidth] = useState(0);
|
||||
const [windowHeight, setWindowHeight] = useState(0);
|
||||
const gridRef = useRef<Grid>(null);
|
||||
const requestedIndicesRef = useRef<Set<number>>(new Set());
|
||||
|
||||
// Track window size
|
||||
useLayoutEffect(() => {
|
||||
function updateSize() {
|
||||
setWindowWidth(window.innerWidth);
|
||||
setWindowHeight(window.innerHeight);
|
||||
}
|
||||
updateSize();
|
||||
window.addEventListener("resize", updateSize);
|
||||
return () => window.removeEventListener("resize", updateSize);
|
||||
}, []);
|
||||
|
||||
// Scroll to top when window width changes (column count changes)
|
||||
useEffect(() => {
|
||||
if (gridRef.current && windowWidth > 0) {
|
||||
gridRef.current.scrollTo({ scrollLeft: 0, scrollTop: 0 });
|
||||
}
|
||||
}, [windowWidth]);
|
||||
|
||||
// Function to request more skins when a cell needs data
|
||||
const requestSkinsIfNeeded = useCallback(
|
||||
(index: number) => {
|
||||
// Only fetch if this index is beyond our current data
|
||||
if (index >= skins.length) {
|
||||
// Calculate which batch this index belongs to
|
||||
const batchSize = 50; // Fetch in batches
|
||||
const batchStart = Math.floor(skins.length / batchSize) * batchSize;
|
||||
|
||||
// Only fetch if we haven't already requested this batch
|
||||
if (!requestedIndicesRef.current.has(batchStart) && !fetching) {
|
||||
requestedIndicesRef.current.add(batchStart);
|
||||
setFetching(true);
|
||||
getSkins(sessionId, batchStart)
|
||||
.then((newSkins) => {
|
||||
setSkins((prevSkins) => [...prevSkins, ...newSkins]);
|
||||
setFetching(false);
|
||||
})
|
||||
.catch(() => {
|
||||
requestedIndicesRef.current.delete(batchStart);
|
||||
setFetching(false);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
[skins.length, fetching, sessionId, getSkins]
|
||||
);
|
||||
|
||||
const { columnWidth, rowHeight, columnCount } =
|
||||
getGridDimensions(windowWidth);
|
||||
|
||||
if (windowWidth === 0 || windowHeight === 0) {
|
||||
return null; // Don't render until we have window dimensions
|
||||
}
|
||||
|
||||
const rowCount = Math.ceil(skins.length / columnCount);
|
||||
|
||||
const itemData = {
|
||||
skins,
|
||||
columnCount,
|
||||
requestSkinsIfNeeded,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={{ backgroundColor: "#1a1a1a" }}>
|
||||
<Grid
|
||||
ref={gridRef}
|
||||
columnCount={columnCount}
|
||||
columnWidth={columnWidth}
|
||||
height={windowHeight}
|
||||
rowCount={rowCount}
|
||||
rowHeight={rowHeight}
|
||||
width={windowWidth}
|
||||
itemData={itemData}
|
||||
overscanRowCount={2}
|
||||
style={{
|
||||
scrollbarWidth: "none", // Firefox
|
||||
msOverflowStyle: "none", // IE and Edge
|
||||
}}
|
||||
className="hide-scrollbar"
|
||||
>
|
||||
{GridCell}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
"use server";
|
||||
|
||||
import { getMuseumPage, getScreenshotUrl } from "../../../../data/skins";
|
||||
|
||||
export type GridSkin = {
|
||||
md5: string;
|
||||
screenshotUrl: string;
|
||||
fileName: string;
|
||||
nsfw: boolean;
|
||||
};
|
||||
|
||||
export async function getMuseumPageSkins(
|
||||
offset: number,
|
||||
limit: number
|
||||
): Promise<GridSkin[]> {
|
||||
const page = await getMuseumPage({ offset, first: limit });
|
||||
|
||||
const skins = page.map((item) => ({
|
||||
md5: item.md5,
|
||||
screenshotUrl: getScreenshotUrl(item.md5),
|
||||
fileName: item.fileName,
|
||||
nsfw: item.nsfw,
|
||||
}));
|
||||
|
||||
return skins;
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import { ReactNode } from "react";
|
||||
|
||||
type LayoutProps = {
|
||||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return <div style={{ width: "100vw" }}>{children}</div>;
|
||||
}
|
||||
13
packages/skin-database/app/(modern)/scroll/grid/page.tsx
Normal file
13
packages/skin-database/app/(modern)/scroll/grid/page.tsx
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import React from "react";
|
||||
import Grid from "./Grid";
|
||||
import { getMuseumPageSkins } from "./getMuseumPageSkins";
|
||||
import * as Skins from "../../../../data/skins";
|
||||
|
||||
export default async function SkinTable() {
|
||||
const [initialSkins, skinCount] = await Promise.all([
|
||||
getMuseumPageSkins(0, 50),
|
||||
Skins.getClassicSkinCount(),
|
||||
]);
|
||||
|
||||
return <Grid initialSkins={initialSkins} initialTotal={skinCount} />;
|
||||
}
|
||||
|
|
@ -8,14 +8,11 @@ type LayoutProps = {
|
|||
children: ReactNode;
|
||||
};
|
||||
|
||||
export default function LayoutWrapper({ children }: LayoutProps) {
|
||||
export default function Layout({ children }: LayoutProps) {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "relative",
|
||||
width: "100vw",
|
||||
maxWidth: "56.25vh", // 9:16 aspect ratio (100vh * 9/16)
|
||||
margin: "0 auto",
|
||||
height: "100vh",
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export const SCREENSHOT_WIDTH = 275;
|
||||
export const SCREENSHOT_HEIGHT = 348;
|
||||
export const SKIN_RATIO = SCREENSHOT_HEIGHT / SCREENSHOT_WIDTH;
|
||||
export const MOBILE_MAX_WIDTH = "56.25vh"; // 9:16 aspect ratio (100vh * 9/16) for TikTok-style scroll
|
||||
export const ABOUT_PAGE = "ABOUT_PAGE";
|
||||
export const UPLOAD_PAGE = "UPLOAD_PAGE";
|
||||
export const REVIEW_PAGE = "REVIEW_PAGE";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
serverExternalPackages: ["knex", "imagemin-optipng", "discord.js"],
|
||||
experimental: {
|
||||
viewTransition: true,
|
||||
},
|
||||
};
|
||||
|
||||
module.exports = nextConfig;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue