Skip to content

Commit 548db7b

Browse files
committed
initial site
1 parent 254f079 commit 548db7b

File tree

3 files changed

+707
-65
lines changed

3 files changed

+707
-65
lines changed

src/App.jsx

Lines changed: 100 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,107 @@
11

22
import './App.css'
3+
// src/App.jsx
4+
import React, { useEffect, useMemo, useState } from 'react'
5+
import itemsData from './data/items'
36

4-
function App() {
7+
const STORAGE_KEY = 'silksong.checklist.v1'
8+
9+
function groupByCategory(items) {
10+
return items.reduce((acc, it) => {
11+
if (!acc[it.category]) acc[it.category] = []
12+
acc[it.category].push(it)
13+
return acc
14+
}, {})
15+
}
16+
17+
function sumCheckedPct(items, checkedSet) {
18+
return items.reduce((sum, it) => (checkedSet.has(it.id) ? sum + Number(it.pct) : sum), 0)
19+
}
20+
21+
export default function App() {
22+
const [checked, setChecked] = useState(() => {
23+
try {
24+
const raw = localStorage.getItem(STORAGE_KEY)
25+
return raw ? new Set(JSON.parse(raw)) : new Set()
26+
} catch {
27+
return new Set()
28+
}
29+
})
30+
31+
useEffect(() => {
32+
try {
33+
localStorage.setItem(STORAGE_KEY, JSON.stringify(Array.from(checked)))
34+
} catch (error) {
35+
console.error('Failed to save to localStorage:', error)
36+
}
37+
}, [checked])
38+
39+
const grouped = useMemo(() => groupByCategory(itemsData), [])
40+
const totalPossible = useMemo(() => itemsData.reduce((s, it) => s + Number(it.pct), 0), [])
41+
const totalChecked = useMemo(() => sumCheckedPct(itemsData, checked), [checked])
42+
const percent = Math.round((totalChecked / totalPossible) * 100) // displayed percent (rounded)
43+
const itemsRemaining = itemsData.length - checked.size
44+
45+
function toggle(id) {
46+
setChecked(prev => {
47+
const next = new Set(prev)
48+
if (next.has(id)) next.delete(id)
49+
else next.add(id)
50+
return next
51+
})
52+
}
53+
54+
function resetAll() {
55+
setChecked(new Set())
56+
}
557

658
return (
7-
<main className="page">
8-
<h1 className='title'>Silksong Checklist</h1>
9-
</main>
59+
<div className="site-root">
60+
<header className="top">
61+
<div className="logo">silksong</div>
62+
<h2 className="subtitle">Checklist</h2>
63+
64+
<div className="summary">
65+
<div className="percent">{percent}%</div>
66+
<div className="meta">
67+
{totalChecked}/{totalPossible} pts • {itemsRemaining} item(s) remaining
68+
<button className="link-btn" onClick={() => setChecked(new Set(itemsData.map(i => i.id))) }>
69+
Check all
70+
</button>
71+
<button className="link-btn" onClick={resetAll}>Uncheck all</button>
72+
</div>
73+
</div>
74+
</header>
75+
76+
<main className="content">
77+
{Object.entries(grouped).map(([category, list]) => (
78+
<section key={category} className="category-card">
79+
<h3 className="category-title">{category}</h3>
80+
<ul className="item-list">
81+
{list.map(it => {
82+
const isChecked = checked.has(it.id)
83+
return (
84+
<li key={it.id} className={`item ${isChecked ? 'done' : ''}`}>
85+
<label>
86+
<input
87+
type="checkbox"
88+
checked={isChecked}
89+
onChange={() => toggle(it.id)}
90+
aria-label={`Mark ${it.name} complete`}
91+
/>
92+
<a href={it.link} target="_blank" rel="noreferrer" className="item-link">{it.name}</a>
93+
</label>
94+
</li>
95+
)
96+
})}
97+
</ul>
98+
</section>
99+
))}
100+
</main>
101+
102+
<footer className="footer">
103+
<small>silksong checklist • built with ❤️</small>
104+
</footer>
105+
</div>
10106
)
11107
}
12-
13-
export default App

0 commit comments

Comments
 (0)