Skip to content

Commit 1408857

Browse files
authored
feat: add settings page
1 parent 91cfcb3 commit 1408857

12 files changed

Lines changed: 956 additions & 21 deletions

src/components/browse-filters.tsx

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { Search, X } from 'lucide-react';
22
import { PAGE_SIZE } from '@/lib/config';
3-
import { useState, useMemo } from 'react';
43
import type { CSSProperties } from 'react';
54
import type { LGTMEntry, Rarity } from '@/lib/lgtm';
5+
import { useState, useMemo, useEffect } from 'react';
66
import { Pagination } from '@/components/pagination';
7+
import { SETTINGS_KEY, loadSettings } from '@/lib/settings';
78
import { RARITY_LABELS, CATEGORY_LABELS, getAllRarities } from '@/lib/lgtm';
89
import { CATEGORY_COLORS, RARITY_COLORS, RARITY_ORDER } from '@/lib/content';
910

@@ -119,12 +120,26 @@ export function BrowseFilters({ entries }: BrowseFiltersProps) {
119120
const allCategories = useMemo(() => [...new Set(entries.map((entry) => entry.category))].sort(), [entries]);
120121

121122
const allRarities = getAllRarities();
123+
const [pageSize, setPageSize] = useState(PAGE_SIZE);
122124
const [search, setSearch] = useState('');
123125
const [activeCategories, setActiveCategories] = useState<Set<string>>(new Set());
124126
const [activeRarities, setActiveRarities] = useState<Set<string>>(new Set());
125127
const [sort, setSort] = useState<SortOption>('alpha');
126128
const [page, setPage] = useState(1);
127129

130+
useEffect(() => {
131+
// eslint-disable-next-line react-hooks/set-state-in-effect
132+
setPageSize(loadSettings().pageSize);
133+
134+
function onStorage(e: StorageEvent) {
135+
if (e.key !== SETTINGS_KEY) return;
136+
setPageSize(loadSettings().pageSize);
137+
}
138+
139+
window.addEventListener('storage', onStorage);
140+
return () => window.removeEventListener('storage', onStorage);
141+
}, []);
142+
128143
function toggleCategory(category: string) {
129144
setActiveCategories((prev) => {
130145
const next = new Set(prev);
@@ -193,10 +208,10 @@ export function BrowseFilters({ entries }: BrowseFiltersProps) {
193208
});
194209
}, [entries, search, activeCategories, activeRarities, sort]);
195210

196-
const totalPages = Math.max(1, Math.ceil(filtered.length / PAGE_SIZE));
211+
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
197212
const clampedPage = Math.min(page, totalPages);
198-
const pageStart = (clampedPage - 1) * PAGE_SIZE;
199-
const paginated = filtered.slice(pageStart, pageStart + PAGE_SIZE);
213+
const pageStart = (clampedPage - 1) * pageSize;
214+
const paginated = filtered.slice(pageStart, pageStart + pageSize);
200215
const hasFilters = isFiltered(search, activeCategories, activeRarities);
201216

202217
function clearAll() {
@@ -332,7 +347,7 @@ export function BrowseFilters({ entries }: BrowseFiltersProps) {
332347
>
333348
<p style={{ color: 'var(--color-text-muted)' }} className="m-0 text-sm">
334349
<strong style={{ color: 'var(--color-text)' }}>{filtered.length}</strong> of {entries.length} entries
335-
{filtered.length > PAGE_SIZE && (
350+
{filtered.length > pageSize && (
336351
<span style={{ color: 'var(--color-text-faint)' }} className="ml-2">
337352
page {clampedPage} of {totalPages}
338353
</span>

src/components/category-entries.tsx

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,33 @@
1-
import { useState } from 'react';
21
import { PAGE_SIZE } from '@/lib/config';
2+
import { useState, useEffect } from 'react';
33
import type { LGTMEntry } from '@/lib/lgtm';
44
import { Pagination } from '@/components/pagination';
5+
import { SETTINGS_KEY, loadSettings } from '@/lib/settings';
56

67
export type CategoryEntriesProps = {
78
entries: LGTMEntry[];
89
};
910

1011
export function CategoryEntries({ entries }: CategoryEntriesProps) {
12+
const [pageSize, setPageSize] = useState(PAGE_SIZE);
1113
const [page, setPage] = useState(1);
12-
const totalPages = Math.ceil(entries.length / PAGE_SIZE);
13-
const start = (page - 1) * PAGE_SIZE;
14-
const slice = entries.slice(start, start + PAGE_SIZE);
14+
15+
useEffect(() => {
16+
// eslint-disable-next-line react-hooks/set-state-in-effect
17+
setPageSize(loadSettings().pageSize);
18+
19+
function onStorage(e: StorageEvent) {
20+
if (e.key !== SETTINGS_KEY) return;
21+
setPageSize(loadSettings().pageSize);
22+
}
23+
24+
window.addEventListener('storage', onStorage);
25+
return () => window.removeEventListener('storage', onStorage);
26+
}, []);
27+
28+
const totalPages = Math.ceil(entries.length / pageSize);
29+
const start = (page - 1) * pageSize;
30+
const slice = entries.slice(start, start + pageSize);
1531

1632
function handlePageChange(pageNumber: number) {
1733
setPage(pageNumber);
@@ -25,11 +41,8 @@ export function CategoryEntries({ entries }: CategoryEntriesProps) {
2541
<a
2642
key={entry.id}
2743
href={`/lgtm/${entry.id}`}
44+
style={{ color: 'inherit', borderColor: 'var(--color-border)' }}
2845
className="flex items-start gap-4 py-4 border-b no-underline transition-colors duration-150"
29-
style={{
30-
color: 'inherit',
31-
borderColor: 'var(--color-border)',
32-
}}
3346
>
3447
<span
3548
className="flex-shrink-0 text-xs pt-[0.2rem] min-w-[2.5rem]"

src/components/legendary-effect.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
import { useEffect } from 'react';
22
import confetti from 'canvas-confetti';
3+
import { loadSettings } from '@/lib/settings';
34

45
export function LegendaryEffect() {
56
useEffect(() => {
7+
if (!loadSettings().confetti) return;
8+
69
confetti({
710
spread: 90,
811
particleCount: 120,

src/components/mascot.tsx

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { Rarity } from '@/lib/lgtm';
22
import type { CSSProperties } from 'react';
33
import type { MascotContext } from '@/lib/mascot-lines';
4+
import { SETTINGS_KEY, loadSettings } from '@/lib/settings';
45
import { pickLine, contextFromPath } from '@/lib/mascot-lines';
56
import { useRef, useState, useEffect, useCallback, Fragment } from 'react';
67

@@ -315,6 +316,21 @@ function cursorStyle(visible: boolean): CSSProperties {
315316
}
316317

317318
export function Mascot({ rarity, isIndex }: MascotProps) {
319+
const [enabled, setEnabled] = useState(true);
320+
321+
useEffect(() => {
322+
// eslint-disable-next-line react-hooks/set-state-in-effect
323+
setEnabled(loadSettings().mascot);
324+
325+
function onStorage(e: StorageEvent) {
326+
if (e.key !== SETTINGS_KEY) return;
327+
setEnabled(loadSettings().mascot);
328+
}
329+
330+
window.addEventListener('storage', onStorage);
331+
return () => window.removeEventListener('storage', onStorage);
332+
}, []);
333+
318334
const [pos, setPos] = useState<Pos>(() => {
319335
const saved = loadSavedPos();
320336
return saved && isPosInViewport(saved) ? saved : defaultPos();
@@ -499,6 +515,8 @@ export function Mascot({ rarity, isIndex }: MascotProps) {
499515
const bobY = prefersReducedMotion ? 0 : bobOffset(idleTick);
500516
const showCursor = isTalking && isStillTyping(shownLine, fullLine);
501517

518+
if (!enabled) return null;
519+
502520
return (
503521
<Fragment>
504522
<div style={bubbleFixedStyle(isTalking, isDragging, pos)}>

0 commit comments

Comments
 (0)