diff --git a/packages/skin-database/app/(modern)/scroll/Grid.tsx b/packages/skin-database/app/(modern)/scroll/Grid.tsx index 0337641f..5158566c 100644 --- a/packages/skin-database/app/(modern)/scroll/Grid.tsx +++ b/packages/skin-database/app/(modern)/scroll/Grid.tsx @@ -4,6 +4,7 @@ import React, { useMemo, useCallback, useRef, + useTransition, // @ts-expect-error - unstable_ViewTransition is not yet in @types/react unstable_ViewTransition as ViewTransition, } from "react"; @@ -17,6 +18,12 @@ import { MOBILE_MAX_WIDTH, } from "../../../legacy-client/src/constants"; import { getMuseumPageSkins, GridSkin } from "./getMuseumPageSkins"; +import { searchSkins as performAlgoliaSearch } from "./algoliaClient"; + +// Simple utility to get screenshot URL (avoiding server-side import) +function getScreenshotUrl(md5: string): string { + return `https://r2.webampskins.org/screenshots/${md5}.png`; +} type CellData = { skins: GridSkin[]; @@ -97,11 +104,67 @@ export default function SkinTable({ }: SkinTableProps) { const { windowWidth, windowHeight } = useWindowSize(); - // Initialize state with server-provided data - const [skins, setSkins] = useState(initialSkins); + // Search input state - separate input value from actual search query + const [inputValue, setInputValue] = useState(""); + + // State for browsing mode + const [browseSkins, setBrowseSkins] = useState(initialSkins); const [loadedPages, setLoadedPages] = useState>(new Set([0])); const isLoadingRef = useRef(false); + // State for search mode + const [searchSkins, setSearchSkins] = useState([]); + const [searchError, setSearchError] = useState(null); + const [searchIsPending, setSearchIsPending] = useState(false); + + // Debounce timer ref + + // Determine which mode we're in based on actual search query, not input + const isSearchMode = inputValue.trim().length > 0; + const skins = isSearchMode ? searchSkins : browseSkins; + const total = isSearchMode ? searchSkins.length : initialTotal; + + // Handle search input change + const handleSearchChange = async (e: React.ChangeEvent) => { + const query = e.target.value; + setInputValue(query); + + // If query is empty, clear results immediately + if (!query || query.trim().length === 0) { + return; + // setSearchQuery(""); + startTransition(() => { + setSearchSkins([]); + setSearchError(null); + }); + return; + } + // return; + + try { + setSearchIsPending(true); + const result = await performAlgoliaSearch(query); + const hits = result.hits as Array<{ + objectID: string; + fileName: string; + nsfw?: boolean; + }>; + const searchResults: GridSkin[] = hits.map((hit) => ({ + md5: hit.objectID, + screenshotUrl: getScreenshotUrl(hit.objectID), + fileName: hit.fileName, + nsfw: hit.nsfw ?? false, + })); + setSearchSkins(searchResults); + } catch (err) { + console.error("Search failed:", err); + setSearchError("Search failed. Please try again."); + setSearchSkins([]); + } finally { + setSearchIsPending(false); + } + }; + const columnCount = Math.round(windowWidth / (SCREENSHOT_WIDTH * 0.9)); const columnWidth = windowWidth / columnCount; const rowHeight = columnWidth * SKIN_RATIO; @@ -109,6 +172,11 @@ export default function SkinTable({ const loadMoreSkins = useCallback( async (startIndex: number) => { + // Don't load more in search mode + if (isSearchMode) { + return; + } + const pageNumber = Math.floor(startIndex / pageSize); // Don't reload if we already have this page @@ -120,7 +188,7 @@ export default function SkinTable({ try { const offset = pageNumber * pageSize; const newSkins = await getMuseumPageSkins(offset, pageSize); - setSkins((prev) => [...prev, ...newSkins]); + setBrowseSkins((prev) => [...prev, ...newSkins]); setLoadedPages((prev) => new Set([...prev, pageNumber])); } catch (error) { console.error("Failed to load skins:", error); @@ -128,20 +196,17 @@ export default function SkinTable({ isLoadingRef.current = false; } }, - [loadedPages, pageSize] + [loadedPages, pageSize, isSearchMode] ); - 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 itemKey = useCallback( + ({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => { + const index = rowIndex * columnCount + columnIndex; + const skin = skins[index]; + return skin ? skin.md5 : `empty-cell-${columnIndex}-${rowIndex}`; + }, + [columnCount, skins] + ); const gridRef = React.useRef(null); const itemRef = React.useRef(0); @@ -152,7 +217,7 @@ export default function SkinTable({ itemRef.current = Math.round(scrollData.scrollTop / rowHeight) * columnCount + half; }; - }, [columnCount, rowHeight, loadMoreSkins]); + }, [columnCount, rowHeight]); const itemData: CellData = useMemo( () => ({ @@ -171,7 +236,7 @@ export default function SkinTable({
-
+
{ - e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.95)"; + e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.65)"; e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.3)"; }} onBlur={(e) => { - e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.85)"; + e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.55)"; e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.2)"; }} /> - +
- - {Cell} - + {/* Error State */} + {isSearchMode && searchError && ( +
+ {searchError} +
+ )} + + {/* Empty Results */} + {isSearchMode && !searchError && skins.length === 0 && ( +
+ No results found for "{inputValue}" +
+ )} + + {/* Grid - show when browsing or when we have results (even while pending) */} + {(!isSearchMode || (!searchError && skins.length > 0)) && ( + + {Cell} + + )} ); } diff --git a/packages/skin-database/app/(modern)/scroll/StaticPage.tsx b/packages/skin-database/app/(modern)/scroll/StaticPage.tsx index 724cc027..ee725ebc 100644 --- a/packages/skin-database/app/(modern)/scroll/StaticPage.tsx +++ b/packages/skin-database/app/(modern)/scroll/StaticPage.tsx @@ -120,6 +120,7 @@ export function Input({ borderRadius: "4px", color: "#fff", fontFamily: "inherit", + boxSizing: "border-box", ...style, }} {...props} @@ -146,6 +147,7 @@ export function Textarea({ fontFamily: "inherit", display: "block", resize: "vertical", + boxSizing: "border-box", ...style, }} {...props} diff --git a/packages/skin-database/app/(modern)/scroll/algoliaClient.ts b/packages/skin-database/app/(modern)/scroll/algoliaClient.ts new file mode 100644 index 00000000..7b9afe00 --- /dev/null +++ b/packages/skin-database/app/(modern)/scroll/algoliaClient.ts @@ -0,0 +1,18 @@ +import { algoliasearch } from "algoliasearch"; + +// Using the legacy hardcoded credentials for client-side search +const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51"); + +export async function searchSkins(query: string) { + const result = await client.searchSingleIndex({ + indexName: "Skins", + searchParams: { + query, + attributesToRetrieve: ["objectID", "fileName", "nsfw"], + attributesToHighlight: [], + hitsPerPage: 1000, + typoTolerance: "min", + }, + }); + return result; +} diff --git a/packages/skin-database/app/(modern)/scroll/layout.tsx b/packages/skin-database/app/(modern)/scroll/layout.tsx index c14a138f..eb760d96 100644 --- a/packages/skin-database/app/(modern)/scroll/layout.tsx +++ b/packages/skin-database/app/(modern)/scroll/layout.tsx @@ -18,7 +18,7 @@ export default function Layout({ children }: LayoutProps) { }} > {children} - + diff --git a/packages/skin-database/app/(modern)/scroll/scroll.css b/packages/skin-database/app/(modern)/scroll/scroll.css index 5ed3b090..fea9dee1 100644 --- a/packages/skin-database/app/(modern)/scroll/scroll.css +++ b/packages/skin-database/app/(modern)/scroll/scroll.css @@ -2,6 +2,12 @@ body { margin: 0; /* Remove default margin */ height: 100vh; /* Set body height to viewport height */ background-color: #1a1a1a; /* Dark charcoal instead of pure black */ + font-family: "MS Sans Serif", "Segoe UI", sans-serif; +} + +input, +button { + font-family: "MS Sans Serif", "Segoe UI", sans-serif; } .scroller::-webkit-scrollbar,