An app for logging board climbs
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: store grade filter in localStorage

+96 -14
+1
scripts/moon.ts
··· 356 356 357 357 // Get access token 358 358 console.log('Authenticating...') 359 + await login() 359 360 const token = await getAccessToken() 360 361 console.log('✓ Authentication successful\n') 361 362
+86 -14
www/routes/home.ts
··· 11 11 12 12 const PAGE_SIZE = 50 13 13 14 + type LogFilter = 'all' | 'sent' | 'unsent' 15 + 14 16 export class HomePage extends HTMLElement { 15 17 private benchmarks: Benchmark[] = [] 16 18 private filtered: Benchmark[] = [] ··· 20 22 private shown = PAGE_SIZE 21 23 private mbType = 0 22 24 private gradeScale: 'french' | 'v' = 'french' 25 + private logFilter: LogFilter = 'all' 23 26 24 27 private gradeLabel(grade: number): string { 25 28 const table = this.gradeScale === 'v' ? GRADE_V : GRADE_FRENCH ··· 30 33 this.mbType = parseInt(localStorage.getItem('mb_type') ?? '0', 10) 31 34 this.gradeScale = (localStorage.getItem('grade_scale') as 'french' | 'v') ?? 32 35 'french' 36 + this.logFilter = (localStorage.getItem('home_filter') as LogFilter) ?? 'all' 37 + this.gradeMin = parseInt(localStorage.getItem('home_grade_min') ?? '0', 10) 38 + this.gradeMax = parseInt(localStorage.getItem('home_grade_max') ?? '16', 10) 33 39 34 40 const titleEl = document.querySelector('#page-title') 35 41 if (titleEl) { ··· 66 72 placeholder="Search by name or setter..." 67 73 autocomplete="off" 68 74 > 75 + <div class="bm-log-filter" id="bm-log-filter"> 76 + ${this.logFilterHtml()} 77 + </div> 69 78 <div class="bm-grade-row"> 70 79 <label class="bm-grade-label" for="bm-grade-min">Grade</label> 71 80 <select id="bm-grade-min" class="bm-grade-select"> 72 - ${this.gradeOptions(this.gradeMin)} 81 + ${this.gradeOptions('min')} 73 82 </select> 74 83 <span class="bm-grade-sep" aria-hidden="true">–</span> 75 84 <select id="bm-grade-max" class="bm-grade-select"> 76 - ${this.gradeOptions(this.gradeMax)} 85 + ${this.gradeOptions('max')} 77 86 </select> 78 87 </div> 79 88 </div> ··· 84 93 ` 85 94 } 86 95 87 - private gradeOptions(selected: number): string { 88 - return Array.from( 89 - { length: 17 }, 90 - (_, i) => 91 - `<option value="${i}" ${i === selected ? 'selected' : ''}>${ 92 - this.gradeLabel(i) 93 - }</option>`, 94 - ).join('') 96 + private logFilterHtml(): string { 97 + const opts: { value: LogFilter; label: string }[] = [ 98 + { value: 'all', label: 'All' }, 99 + { value: 'sent', label: 'Completed' }, 100 + { value: 'unsent', label: 'Not Completed' }, 101 + ] 102 + return opts 103 + .map( 104 + (o) => 105 + `<button class="lb-filter-btn${ 106 + this.logFilter === o.value ? ' lb-filter-btn--active' : '' 107 + }" data-log-filter="${o.value}">${o.label}</button>`, 108 + ) 109 + .join('') 110 + } 111 + 112 + private gradeGroups(): { label: string; first: number; last: number }[] { 113 + const table = this.gradeScale === 'v' ? GRADE_V : GRADE_FRENCH 114 + const groups: { label: string; first: number; last: number }[] = [] 115 + for (let i = 0; i <= 16; i++) { 116 + const label = table[i] ?? '?' 117 + const prev = groups[groups.length - 1] 118 + if (prev && prev.label === label) { 119 + prev.last = i 120 + } else { 121 + groups.push({ label, first: i, last: i }) 122 + } 123 + } 124 + return groups 125 + } 126 + 127 + private gradeOptions(role: 'min' | 'max'): string { 128 + const current = role === 'min' ? this.gradeMin : this.gradeMax 129 + return this.gradeGroups() 130 + .map((g) => { 131 + const value = role === 'min' ? g.first : g.last 132 + const selected = current >= g.first && current <= g.last 133 + return `<option value="${value}" ${ 134 + selected ? 'selected' : '' 135 + }>${g.label}</option>` 136 + }) 137 + .join('') 95 138 } 96 139 97 140 private bindListEvents(): void { ··· 108 151 const val = parseInt((e.target as HTMLSelectElement).value, 10) 109 152 this.gradeMin = val 110 153 if (this.gradeMax < val) { 111 - this.gradeMax = val 154 + // snap max to the last index of the group that contains the new min 155 + const group = this.gradeGroups().find( 156 + (g) => g.first <= val && val <= g.last, 157 + ) 158 + this.gradeMax = group?.last ?? val 112 159 const maxEl = this.querySelector<HTMLSelectElement>('#bm-grade-max') 113 - if (maxEl) maxEl.value = val.toString() 160 + if (maxEl) maxEl.value = this.gradeMax.toString() 114 161 } 162 + localStorage.setItem('home_grade_min', this.gradeMin.toString()) 163 + localStorage.setItem('home_grade_max', this.gradeMax.toString()) 115 164 this.shown = PAGE_SIZE 116 165 this.applyFilters() 117 166 this.renderList() ··· 122 171 const val = parseInt((e.target as HTMLSelectElement).value, 10) 123 172 this.gradeMax = val 124 173 if (this.gradeMin > val) { 125 - this.gradeMin = val 174 + // snap min to the first index of the group that contains the new max 175 + const group = this.gradeGroups().find( 176 + (g) => g.first <= val && val <= g.last, 177 + ) 178 + this.gradeMin = group?.first ?? val 126 179 const minEl = this.querySelector<HTMLSelectElement>('#bm-grade-min') 127 - if (minEl) minEl.value = val.toString() 180 + if (minEl) minEl.value = this.gradeMin.toString() 128 181 } 182 + localStorage.setItem('home_grade_min', this.gradeMin.toString()) 183 + localStorage.setItem('home_grade_max', this.gradeMax.toString()) 184 + this.shown = PAGE_SIZE 185 + this.applyFilters() 186 + this.renderList() 187 + }) 188 + 189 + this.querySelector('#bm-log-filter')?.addEventListener('click', (e) => { 190 + const btn = (e.target as HTMLElement).closest<HTMLElement>( 191 + '[data-log-filter]', 192 + ) 193 + if (!btn) return 194 + this.logFilter = btn.dataset.logFilter as LogFilter 195 + localStorage.setItem('home_filter', this.logFilter) 196 + const bar = this.querySelector('#bm-log-filter') 197 + if (bar) bar.innerHTML = this.logFilterHtml() 129 198 this.shown = PAGE_SIZE 130 199 this.applyFilters() 131 200 this.renderList() ··· 150 219 151 220 private applyFilters(): void { 152 221 const q = this.search.toLowerCase() 222 + const logbook = this.logFilter !== 'all' ? loadLogbook() : null 153 223 this.filtered = this.benchmarks.filter((b) => { 154 224 if (b.grade < this.gradeMin || b.grade > this.gradeMax) return false 155 225 if ( ··· 159 229 ) { 160 230 return false 161 231 } 232 + if (this.logFilter === 'sent') return !!logbook![b.id.toString()]?.sent 233 + if (this.logFilter === 'unsent') return !logbook![b.id.toString()]?.sent 162 234 return true 163 235 }) 164 236 }
+2
www/routes/library.ts
··· 75 75 connectedCallback() { 76 76 this.gradeScale = (localStorage.getItem('grade_scale') as 'french' | 'v') ?? 77 77 'french' 78 + this.filter = (localStorage.getItem('library_filter') as Filter) ?? 'all' 78 79 this.innerHTML = ` 79 80 <div class="lb-page"> 80 81 <div class="lb-filter-bar" id="lb-filter-bar"> ··· 97 98 const filterBtn = target.closest<HTMLElement>('[data-filter]') 98 99 if (filterBtn) { 99 100 this.filter = filterBtn.dataset.filter as Filter 101 + localStorage.setItem('library_filter', this.filter) 100 102 const filterBar = this.querySelector('#lb-filter-bar') 101 103 if (filterBar) filterBar.innerHTML = this.filterBarHtml() 102 104 this.renderList()
+7
www/static/theme.css
··· 292 292 flex-shrink: 0; 293 293 } 294 294 295 + .bm-log-filter { 296 + display: flex; 297 + gap: var(--s2); 298 + margin-top: var(--s2); 299 + margin-bottom: var(--s2); 300 + } 301 + 295 302 .bm-list { 296 303 flex: 1; 297 304 }