diff --git a/web/src/components/AppRouter.tsx b/web/src/components/AppRouter.tsx index a372be7..bba9085 100644 --- a/web/src/components/AppRouter.tsx +++ b/web/src/components/AppRouter.tsx @@ -6,12 +6,6 @@ import { getBookNamesMap, getChapterData } from '../utils/dataUtils' import { groupByBook, groupByDay } from '../utils/groupUtils' import { useApi } from './ApiContext' import { createMemo } from 'solid-js' -import type { Tag } from '../data/model' - -const OT_NT = { - OT: 3, - NT: 2, -} as Record export function AppRouter() { const api = useApi() @@ -20,7 +14,7 @@ export function AppRouter() { const bookNames = getBookNamesMap(chapters) const bookGroups = groupByBook(chapters) const planGroups = createMemo(() => - groupByDay(chapters, api.getTags(), OT_NT), + groupByDay(chapters, api.getTags(), api.perDayTagData(), api.targetDays()), ) return ( diff --git a/web/src/components/PlanSettings.tsx b/web/src/components/PlanSettings.tsx index dc69250..d982145 100644 --- a/web/src/components/PlanSettings.tsx +++ b/web/src/components/PlanSettings.tsx @@ -72,6 +72,41 @@ export function PlanSettings() { +
+ + + +
+

Data

diff --git a/web/src/components/TagSelector.tsx b/web/src/components/TagSelector.tsx index 55824ea..2d7d234 100644 --- a/web/src/components/TagSelector.tsx +++ b/web/src/components/TagSelector.tsx @@ -24,12 +24,33 @@ export function TagSelector(props: TagSelectorProps) { } } + const onRemove = (tagName: Tag) => { + return () => { + props.onChange({ + ...props.value, + tags: props.value.tags.filter((t) => t !== tagName), + }) + } + } + return (
- + props.onChange({ ...props.value, count: +e.target.value })} + />
    - {(tagName) =>
  • {tagName}
  • }
    + + {(tagName) => ( +
  • + {tagName} + +
  • + )} +
  • ([ - { - tags: ['OT' as Tag], - count: 3, - }, - { - tags: ['NT' as Tag], - count: 2, - }, - ]) + const [perDayTagData, setPerDayTagData] = createSignal( + settingsData.perDayTagData, + ) this.perDayTagData = perDayTagData - this.setPerDayTagData = setPerDayTagData + this._setPerDayTagData = setPerDayTagData + + // targetDays signal + const [targetDays, setTargetDays] = createSignal(settingsData.targetDays) + this.targetDays = targetDays + this._setTargetDays = setTargetDays + + // cutoffDays signal + const [cutoffDays, setCutoffDays] = createSignal(settingsData.cutoffDays) + this.cutoffDays = cutoffDays + this._setCutoffDays = setCutoffDays + + // cutoffDate signal + const [cutoffDate, setCutoffDate] = createSignal(settingsData.cutoffDate) + this.cutoffDate = cutoffDate + this._setCutoffDate = setCutoffDate // searchText signal const [searchText, setSearchText] = createSignal('') @@ -80,15 +88,38 @@ export class Api { private readonly _settingsData: SettingsData private readonly _tagsData: TagRecord private readonly _setShowCompleted: Setter + private readonly _setTargetDays: Setter + private readonly _setCutoffDays: Setter + private readonly _setCutoffDate: Setter + private readonly _setPerDayTagData: Setter private readonly _setTimeStampMap: Setter readonly perDayTagData: Accessor - readonly setPerDayTagData: Setter + readonly targetDays: Accessor + readonly cutoffDays: Accessor + readonly cutoffDate: Accessor readonly searchText: Accessor readonly setSearchText: Setter readonly showCompleted: Accessor readonly timeStampMap: Accessor + private currentSettings = (): SettingsData => ({ + showCompleted: this.showCompleted(), + targetDays: this.targetDays(), + cutoffDays: this.cutoffDays(), + cutoffDate: this.cutoffDate(), + perDayTagData: this.perDayTagData(), + }) + + private effectiveCutoff = (): string | null => { + const days = this.cutoffDays() + const rolling = days != null + ? new Date(Date.now() - days * 86400000).toISOString().slice(0, 10) + : null + const candidates = [rolling, this.cutoffDate()].filter(Boolean) as string[] + return candidates.length ? candidates.toSorted().at(-1)! : null + } + getChapterData = (): ChapterData[] => { return this._chapterData } @@ -139,7 +170,10 @@ export class Api { abbrev, number, }: Pick) => { - return this.getChapterDates({ abbrev, number }).length > 0 + const dates = this.getChapterDates({ abbrev, number }) + const cutoff = this.effectiveCutoff() + if (!cutoff) return dates.length > 0 + return dates.some((d) => d.slice(0, 10) >= cutoff) } completeCount = (chapters: ChapterData[]): number => { @@ -173,9 +207,31 @@ export class Api { deleteTimeStamp(this._db, book, chapter, date) } + setPerDayTagData = async ( + valueOrUpdater: PerDayTagData[] | ((prev: PerDayTagData[]) => PerDayTagData[]), + ) => { + this._setPerDayTagData(valueOrUpdater as PerDayTagData[]) + await updateSettings(this._db, this.currentSettings()) + } + + setTargetDays = async (value: number) => { + this._setTargetDays(value) + await updateSettings(this._db, this.currentSettings()) + } + + setCutoffDays = async (value: number | null) => { + this._setCutoffDays(value) + await updateSettings(this._db, this.currentSettings()) + } + + setCutoffDate = async (value: string | null) => { + this._setCutoffDate(value) + await updateSettings(this._db, this.currentSettings()) + } + toggleShowCompleted = async () => { this._setShowCompleted((prev) => !prev) - await updateSettings(this._db, this.showCompleted()) + await updateSettings(this._db, this.currentSettings()) } exportData = (): string => { diff --git a/web/src/data/indexDb.ts b/web/src/data/indexDb.ts index 6d18bdb..d8c40af 100644 --- a/web/src/data/indexDb.ts +++ b/web/src/data/indexDb.ts @@ -2,7 +2,9 @@ import type { BookAbbrev, ChapterID, ISODateTimeString, + PerDayTagData, SettingsData, + Tag, TimeStampMap, } from './model' @@ -14,6 +16,10 @@ export type TimeStampKey = `${ISODateTimeString}_${BookAbbrev}_${ChapterID}` interface SettingsRecord { id: '1' showCompleted: boolean + targetDays?: number + cutoffDays?: number | null + cutoffDate?: string | null + perDayTagData?: PerDayTagData[] } interface TimestampRecord { @@ -110,10 +116,10 @@ export async function addTimestamp( } /** Update the settings record */ -export async function updateSettings(db: IDBDatabase, showCompleted: boolean) { +export async function updateSettings(db: IDBDatabase, settings: SettingsData) { const record: SettingsRecord = { id: '1', - showCompleted, + ...settings, } return putRecord(db, SETTINGS_STORE_NAME, record) @@ -157,6 +163,13 @@ export async function getSettingsData(db: IDBDatabase): Promise { const result = (event.target as IDBRequest).result resolve({ showCompleted: result?.showCompleted ?? true, + targetDays: result?.targetDays ?? 365, + cutoffDays: result?.cutoffDays ?? null, + cutoffDate: result?.cutoffDate ?? null, + perDayTagData: result?.perDayTagData ?? [ + { tags: ['OT' as Tag], count: 3 }, + { tags: ['NT' as Tag], count: 2 }, + ], }) } diff --git a/web/src/data/model.ts b/web/src/data/model.ts index 3be28d4..37655cd 100644 --- a/web/src/data/model.ts +++ b/web/src/data/model.ts @@ -32,6 +32,10 @@ export type TagRecord = Record> export interface SettingsData { showCompleted: boolean + targetDays: number + cutoffDays: number | null + cutoffDate: string | null + perDayTagData: PerDayTagData[] } export interface PerDayTagData { diff --git a/web/src/utils/groupUtils.ts b/web/src/utils/groupUtils.ts index 58450ad..127e904 100644 --- a/web/src/utils/groupUtils.ts +++ b/web/src/utils/groupUtils.ts @@ -1,5 +1,4 @@ -import type { BookName, ChapterData, Tag, TagRecord } from '../data/model' -import { keys } from './dataUtils' +import type { BookName, ChapterData, PerDayTagData, TagRecord } from '../data/model' export function groupByBook( data: ChapterData[], @@ -15,45 +14,27 @@ export function groupByBook( export function groupByDay( chapters: ChapterData[], tagRecord: TagRecord, - tagPerDay: Record, + perDayTagData: PerDayTagData[], + targetDays: number, ): Record { - console.log('[groupByDay]', { chapters, tagRecord, tagPerDay }) const groups: Record = {} - const dayTags = keys(tagPerDay) + const pools: ChapterData[][] = perDayTagData.map((entry) => + entry.tags.flatMap((tag) => chapters.filter((ch) => tagRecord[tag]?.[ch.abbrev])) + ) + const cursors: number[] = pools.map(() => 0) - let totalDays = 0 - const cursors: Record = {} - const tagChapters: Record = {} - - for (const tag of dayTags) { - cursors[tag] = 0 - tagChapters[tag] = chapters.filter((ch) => tagRecord[tag][ch.abbrev]) - - // total days is the minimum number of buckets needed to fit the longest tag - // chapter / tags per day count - totalDays = Math.max( - totalDays, - Math.ceil(tagChapters[tag].length / tagPerDay[tag]), - ) - } - - for (let i = 0; i < totalDays; i++) { + for (let day = 0; day < targetDays; day++) { const group: ChapterData[] = [] - - for (const tag of dayTags) { - for (let j = 0; j < tagPerDay[tag]; j++) { - const c = cursors[tag] % tagChapters[tag].length - // for last day, make sure we don't loop the longest tag - if (i === totalDays - 1 && c < cursors[tag]) { - break - } - cursors[tag] = c + 1 - group.push(tagChapters[tag][c]) + for (let i = 0; i < perDayTagData.length; i++) { + const pool = pools[i] + if (!pool.length) continue + for (let j = 0; j < perDayTagData[i].count; j++) { + group.push(pool[cursors[i] % pool.length]) + cursors[i]++ } } - - groups[`Day ${i + 1}`] = group + groups[`Day ${day + 1}`] = group } return groups