Skip to content

Commit 04c396e

Browse files
committed
Scroll search
1 parent f74d7a6 commit 04c396e

File tree

5 files changed

+167
-46
lines changed

5 files changed

+167
-46
lines changed

packages/skin-database/app/(modern)/scroll/Grid.tsx

Lines changed: 140 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import React, {
44
useMemo,
55
useCallback,
66
useRef,
7+
useTransition,
78
// @ts-expect-error - unstable_ViewTransition is not yet in @types/react
89
unstable_ViewTransition as ViewTransition,
910
} from "react";
@@ -17,6 +18,12 @@ import {
1718
MOBILE_MAX_WIDTH,
1819
} from "../../../legacy-client/src/constants";
1920
import { getMuseumPageSkins, GridSkin } from "./getMuseumPageSkins";
21+
import { searchSkins as performAlgoliaSearch } from "./algoliaClient";
22+
23+
// Simple utility to get screenshot URL (avoiding server-side import)
24+
function getScreenshotUrl(md5: string): string {
25+
return `https://r2.webampskins.org/screenshots/${md5}.png`;
26+
}
2027

2128
type CellData = {
2229
skins: GridSkin[];
@@ -97,18 +104,79 @@ export default function SkinTable({
97104
}: SkinTableProps) {
98105
const { windowWidth, windowHeight } = useWindowSize();
99106

100-
// Initialize state with server-provided data
101-
const [skins, setSkins] = useState<GridSkin[]>(initialSkins);
107+
// Search input state - separate input value from actual search query
108+
const [inputValue, setInputValue] = useState("");
109+
110+
// State for browsing mode
111+
const [browseSkins, setBrowseSkins] = useState<GridSkin[]>(initialSkins);
102112
const [loadedPages, setLoadedPages] = useState<Set<number>>(new Set([0]));
103113
const isLoadingRef = useRef(false);
104114

115+
// State for search mode
116+
const [searchSkins, setSearchSkins] = useState<GridSkin[]>([]);
117+
const [searchError, setSearchError] = useState<string | null>(null);
118+
const [searchIsPending, setSearchIsPending] = useState(false);
119+
120+
// Debounce timer ref
121+
122+
// Determine which mode we're in based on actual search query, not input
123+
const isSearchMode = inputValue.trim().length > 0;
124+
const skins = isSearchMode ? searchSkins : browseSkins;
125+
const total = isSearchMode ? searchSkins.length : initialTotal;
126+
127+
// Handle search input change
128+
const handleSearchChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
129+
const query = e.target.value;
130+
setInputValue(query);
131+
132+
// If query is empty, clear results immediately
133+
if (!query || query.trim().length === 0) {
134+
return;
135+
// setSearchQuery("");
136+
startTransition(() => {
137+
setSearchSkins([]);
138+
setSearchError(null);
139+
});
140+
return;
141+
}
142+
// return;
143+
144+
try {
145+
setSearchIsPending(true);
146+
const result = await performAlgoliaSearch(query);
147+
const hits = result.hits as Array<{
148+
objectID: string;
149+
fileName: string;
150+
nsfw?: boolean;
151+
}>;
152+
const searchResults: GridSkin[] = hits.map((hit) => ({
153+
md5: hit.objectID,
154+
screenshotUrl: getScreenshotUrl(hit.objectID),
155+
fileName: hit.fileName,
156+
nsfw: hit.nsfw ?? false,
157+
}));
158+
setSearchSkins(searchResults);
159+
} catch (err) {
160+
console.error("Search failed:", err);
161+
setSearchError("Search failed. Please try again.");
162+
setSearchSkins([]);
163+
} finally {
164+
setSearchIsPending(false);
165+
}
166+
};
167+
105168
const columnCount = Math.round(windowWidth / (SCREENSHOT_WIDTH * 0.9));
106169
const columnWidth = windowWidth / columnCount;
107170
const rowHeight = columnWidth * SKIN_RATIO;
108171
const pageSize = 50; // Number of skins to load per page
109172

110173
const loadMoreSkins = useCallback(
111174
async (startIndex: number) => {
175+
// Don't load more in search mode
176+
if (isSearchMode) {
177+
return;
178+
}
179+
112180
const pageNumber = Math.floor(startIndex / pageSize);
113181

114182
// Don't reload if we already have this page
@@ -120,28 +188,25 @@ export default function SkinTable({
120188
try {
121189
const offset = pageNumber * pageSize;
122190
const newSkins = await getMuseumPageSkins(offset, pageSize);
123-
setSkins((prev) => [...prev, ...newSkins]);
191+
setBrowseSkins((prev) => [...prev, ...newSkins]);
124192
setLoadedPages((prev) => new Set([...prev, pageNumber]));
125193
} catch (error) {
126194
console.error("Failed to load skins:", error);
127195
} finally {
128196
isLoadingRef.current = false;
129197
}
130198
},
131-
[loadedPages, pageSize]
199+
[loadedPages, pageSize, isSearchMode]
132200
);
133201

134-
function itemKey({
135-
columnIndex,
136-
rowIndex,
137-
}: {
138-
columnIndex: number;
139-
rowIndex: number;
140-
}) {
141-
const index = rowIndex * columnCount + columnIndex;
142-
const skin = skins[index];
143-
return skin ? skin.md5 : `empty-cell-${columnIndex}-${rowIndex}`;
144-
}
202+
const itemKey = useCallback(
203+
({ columnIndex, rowIndex }: { columnIndex: number; rowIndex: number }) => {
204+
const index = rowIndex * columnCount + columnIndex;
205+
const skin = skins[index];
206+
return skin ? skin.md5 : `empty-cell-${columnIndex}-${rowIndex}`;
207+
},
208+
[columnCount, skins]
209+
);
145210

146211
const gridRef = React.useRef<any>(null);
147212
const itemRef = React.useRef<number>(0);
@@ -152,7 +217,7 @@ export default function SkinTable({
152217
itemRef.current =
153218
Math.round(scrollData.scrollTop / rowHeight) * columnCount + half;
154219
};
155-
}, [columnCount, rowHeight, loadMoreSkins]);
220+
}, [columnCount, rowHeight]);
156221

157222
const itemData: CellData = useMemo(
158223
() => ({
@@ -171,7 +236,7 @@ export default function SkinTable({
171236
<div
172237
style={{
173238
position: "fixed",
174-
bottom: "5rem",
239+
bottom: "4.25rem",
175240
left: "50%",
176241
transform: "translateX(-50%)",
177242
width: "calc(100% - 2rem)",
@@ -180,58 +245,88 @@ export default function SkinTable({
180245
zIndex: 998,
181246
}}
182247
>
183-
<form
184-
action="/"
185-
method="GET"
186-
style={{
187-
width: "100%",
188-
}}
189-
>
248+
<div style={{ position: "relative" }}>
190249
<input
191250
type="search"
192-
name="q"
251+
value={inputValue}
252+
onChange={handleSearchChange}
193253
placeholder="Search skins..."
194254
style={{
195255
width: "100%",
196256
padding: "0.75rem 1rem",
257+
paddingRight: "1rem",
197258
fontSize: "1rem",
198-
backgroundColor: "rgba(26, 26, 26, 0.85)",
259+
backgroundColor: "rgba(26, 26, 26, 0.55)",
199260
backdropFilter: "blur(10px)",
200261
border: "1px solid rgba(255, 255, 255, 0.2)",
201262
borderRadius: "9999px",
202263
color: "#fff",
203264
outline: "none",
204265
fontFamily: "inherit",
205266
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.3)",
267+
transition: "padding-right 0.2s ease",
206268
}}
207269
onFocus={(e) => {
208-
e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.95)";
270+
e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.65)";
209271
e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.3)";
210272
}}
211273
onBlur={(e) => {
212-
e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.85)";
274+
e.currentTarget.style.backgroundColor = "rgba(26, 26, 26, 0.55)";
213275
e.currentTarget.style.borderColor = "rgba(255, 255, 255, 0.2)";
214276
}}
215277
/>
216-
</form>
278+
</div>
217279
</div>
218280

219-
<Grid
220-
ref={gridRef}
221-
itemKey={itemKey}
222-
itemData={itemData}
223-
columnCount={columnCount}
224-
columnWidth={columnWidth}
225-
height={windowHeight}
226-
rowCount={Math.ceil(initialTotal / columnCount)}
227-
rowHeight={rowHeight}
228-
width={windowWidth}
229-
overscanRowsCount={5}
230-
onScroll={onScroll}
231-
style={{ overflowY: "scroll" }}
232-
>
233-
{Cell}
234-
</Grid>
281+
{/* Error State */}
282+
{isSearchMode && searchError && (
283+
<div
284+
style={{
285+
display: "flex",
286+
justifyContent: "center",
287+
alignItems: "center",
288+
height: windowHeight,
289+
color: "#ff6b6b",
290+
}}
291+
>
292+
{searchError}
293+
</div>
294+
)}
295+
296+
{/* Empty Results */}
297+
{isSearchMode && !searchError && skins.length === 0 && (
298+
<div
299+
style={{
300+
display: "flex",
301+
justifyContent: "center",
302+
alignItems: "center",
303+
height: windowHeight,
304+
color: "#ccc",
305+
}}
306+
>
307+
No results found for &quot;{inputValue}&quot;
308+
</div>
309+
)}
310+
311+
{/* Grid - show when browsing or when we have results (even while pending) */}
312+
{(!isSearchMode || (!searchError && skins.length > 0)) && (
313+
<Grid
314+
ref={gridRef}
315+
itemKey={itemKey}
316+
itemData={itemData}
317+
columnCount={columnCount}
318+
columnWidth={columnWidth}
319+
height={windowHeight}
320+
rowCount={Math.ceil(total / columnCount)}
321+
rowHeight={rowHeight}
322+
width={windowWidth}
323+
overscanRowsCount={5}
324+
onScroll={onScroll}
325+
style={{ overflowY: "scroll" }}
326+
>
327+
{Cell}
328+
</Grid>
329+
)}
235330
</div>
236331
);
237332
}

packages/skin-database/app/(modern)/scroll/StaticPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ export function Input({
120120
borderRadius: "4px",
121121
color: "#fff",
122122
fontFamily: "inherit",
123+
boxSizing: "border-box",
123124
...style,
124125
}}
125126
{...props}
@@ -146,6 +147,7 @@ export function Textarea({
146147
fontFamily: "inherit",
147148
display: "block",
148149
resize: "vertical",
150+
boxSizing: "border-box",
149151
...style,
150152
}}
151153
{...props}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { algoliasearch } from "algoliasearch";
2+
3+
// Using the legacy hardcoded credentials for client-side search
4+
const client = algoliasearch("HQ9I5Z6IM5", "6466695ec3f624a5fccf46ec49680e51");
5+
6+
export async function searchSkins(query: string) {
7+
const result = await client.searchSingleIndex({
8+
indexName: "Skins",
9+
searchParams: {
10+
query,
11+
attributesToRetrieve: ["objectID", "fileName", "nsfw"],
12+
attributesToHighlight: [],
13+
hitsPerPage: 1000,
14+
typoTolerance: "min",
15+
},
16+
});
17+
return result;
18+
}

packages/skin-database/app/(modern)/scroll/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default function Layout({ children }: LayoutProps) {
1818
}}
1919
>
2020
{children}
21-
<ViewTransition name="footer">
21+
<ViewTransition>
2222
<BottomMenuBar />
2323
</ViewTransition>
2424
</div>

packages/skin-database/app/(modern)/scroll/scroll.css

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@ body {
22
margin: 0; /* Remove default margin */
33
height: 100vh; /* Set body height to viewport height */
44
background-color: #1a1a1a; /* Dark charcoal instead of pure black */
5+
font-family: "MS Sans Serif", "Segoe UI", sans-serif;
6+
}
7+
8+
input,
9+
button {
10+
font-family: "MS Sans Serif", "Segoe UI", sans-serif;
511
}
612

713
.scroller::-webkit-scrollbar,

0 commit comments

Comments
 (0)