this repo has no description
0
fork

Configure Feed

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

feat: add streak counter

+96 -4
+1 -1
deno.json
··· 1 1 { 2 - "version": "3.5.0", 2 + "version": "3.5.1", 3 3 "workspace": ["./data"], 4 4 "compilerOptions": { 5 5 "lib": [
+48
www/components/h-streak.ts
··· 1 + import { html, LitElement } from 'lit' 2 + import app from '$/models/app.ts' 3 + import getString from '$/utils/get_string.ts' 4 + 5 + /** 6 + * Streak counter: current streak and longest streak. 7 + * 8 + * @element h-streak 9 + */ 10 + export class HStreak extends LitElement { 11 + #onUpdate = () => this.requestUpdate() 12 + 13 + override createRenderRoot() { 14 + return this 15 + } 16 + 17 + override connectedCallback() { 18 + super.connectedCallback() 19 + app.addEventListener(this.#onUpdate) 20 + } 21 + 22 + override disconnectedCallback() { 23 + super.disconnectedCallback() 24 + app.removeEventListener(this.#onUpdate) 25 + } 26 + 27 + override render() { 28 + const { currStreak = 0, longestStreak = 0 } = app.state.stats ?? {} 29 + return html` 30 + <section class="streak tc pa2"> 31 + <p class="f4 b ma1"> 32 + <span class="streak-count">${getString( 33 + 'studied', 34 + )} ${currStreak}</span> 35 + ${getString('days_in_a_row')}${currStreak < longestStreak 36 + ? html` 37 + <span class="o-60 f5">(${getString( 38 + 'longest_streak', 39 + )}: ${longestStreak})</span> 40 + ` 41 + : null}! 42 + </p> 43 + </section> 44 + ` 45 + } 46 + } 47 + 48 + if (!customElements.get('h-streak')) customElements.define('h-streak', HStreak)
+2
www/components/mod.ts
··· 12 12 import './h-reference-header.ts' 13 13 import './h-reference-subhead.ts' 14 14 import './h-stats.ts' 15 + import './h-streak.ts' 15 16 16 17 export * from './h-answer-input.ts' 17 18 export * from './h-text-reading.ts' ··· 26 27 export * from './h-reference-header.ts' 27 28 export * from './h-reference-subhead.ts' 28 29 export * from './h-stats.ts' 30 + export * from './h-streak.ts' 29 31 30 32 import '../routes/games.ts' 31 33 import '../routes/games/sentences.ts'
+2 -1
www/models/schema.ts
··· 9 9 Override, 10 10 OverrideProps, 11 11 Settings, 12 + type Stats, 12 13 StoreState, 13 14 USER_LANGS, 14 15 type UserLang, ··· 178 179 USER_LANGS, 179 180 UserLang, 180 181 } 181 - export type { JournalEntry } 182 + export type { JournalEntry, Stats }
+9
www/models/schema/v3.ts
··· 27 27 journal: z.array(JournalEntry).default([]), 28 28 }) 29 29 30 + export const Stats = z.object({ 31 + currStreak: z.number().default(0), 32 + longestStreak: z.number().default(0), 33 + lastStudiedDate: z.string().optional(), 34 + }) 35 + 36 + export type Stats = z.infer<typeof Stats> 37 + 30 38 export const StoreState = V2StoreState.extend({ 31 39 games: Games.default(() => Games.parse({})), 40 + stats: Stats.default(() => Stats.parse({})), 32 41 }) 33 42 34 43 export type StoreState = z.infer<typeof StoreState>
+1
www/routes/main.ts
··· 78 78 deck.learnable.length, 79 79 )} ${this.#renderStudyButton(SessionType.Quiz, deck.quizzable.length)} 80 80 </div> 81 + <h-streak></h-streak> 81 82 <h-stats></h-stats> 82 83 ` 83 84 }
+2
www/routes/study.ts
··· 19 19 import { getColor, getSubjectTileProps } from '$/utils/subject_utils.ts' 20 20 import { playAudio } from '$/utils/audio.ts' 21 21 import { checkSuccess } from '$/utils/check_answer.ts' 22 + import { updateStreak } from '$/utils/streak.ts' 22 23 import getString from '$/utils/get_string.ts' 23 24 import type { HAnswerInput } from '$/components/h-answer-input.ts' 24 25 ··· 99 100 Assignment 100 101 > 101 102 await app.persist() 103 + await updateStreak() 102 104 103 105 const answerInputEl = this.querySelector('h-answer-input') as 104 106 | HAnswerInput
+3
www/static/strings/en.json
··· 56 56 "mnemonic": "Mnemonic", 57 57 "new": "New", 58 58 "no_games": "No games available for this language.", 59 + "days_in_a_row": "days in a row", 60 + "longest_streak": "longest streak", 59 61 "no_new_lessons": "No new lessons", 60 62 "no_new_reviews": "No new reviews", 61 63 "open_source": "Hanzi Offline is Open Source", ··· 80 82 "settings": "Settings", 81 83 "show_answer": "Show Answer", 82 84 "sound": "Sound", 85 + "studied": "Studied", 83 86 "study": "Study", 84 87 "study_group_size": "Study Group Size", 85 88 "study_group_size_hint": "The maximum number of items to study at a time.",
+3 -2
www/static/styles/theme.css
··· 83 83 --shadow-xl: var(--black) 3px 3px 0px 0px; 84 84 85 85 /* Hanzi-specific shadows */ 86 - --purple-shadow: hsl(var(--purpleH), var(--purpleS), var(--purpleL)) 3px 3px 0px 0px; 86 + --purple-shadow: hsl(var(--purpleH), var(--purpleS), var(--purpleL)) 3px 3px 87 + 0px 0px; 87 88 --blue-shadow: hsl(var(--blueH), var(--blueS), var(--blueL)) 3px 3px 0px 0px; 88 89 --pink-shadow: hsl(var(--pinkH), var(--pinkS), var(--pinkL)) 3px 3px 0px 0px; 89 90 ··· 366 367 gap: var(--s3); 367 368 margin: 0 auto; 368 369 max-width: var(--main-max-width); 369 - padding: var(--s2) 0 var(--s5) 0; 370 + padding: var(--s2) 0 var(--s2) 0; 370 371 } 371 372 372 373 .study-button,
+25
www/utils/streak.ts
··· 1 + import app from '$/models/app.ts' 2 + 3 + function toDateStr(date: Date): string { 4 + return date.toISOString().slice(0, 10) 5 + } 6 + 7 + /** 8 + * Update streak based on today's date. Idempotent: safe to call on every card 9 + * submission — if already recorded today, it's a no-op. 10 + */ 11 + export async function updateStreak(): Promise<void> { 12 + const today = toDateStr(new Date()) 13 + const stats = app.state.stats 14 + if (stats.lastStudiedDate === today) return 15 + 16 + const yesterday = toDateStr(new Date(Date.now() - 864e5)) 17 + const prev = stats.currStreak ?? 0 18 + 19 + stats.currStreak = stats.lastStudiedDate === yesterday ? prev + 1 : 1 20 + stats.longestStreak = Math.max(stats.longestStreak ?? 0, stats.currStreak) 21 + stats.lastStudiedDate = today 22 + 23 + await app.persist() 24 + app.notify() 25 + }