Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions web/src/components/AppRouter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<Tag, number>

export function AppRouter() {
const api = useApi()
Expand All @@ -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 (
Expand Down
35 changes: 35 additions & 0 deletions web/src/components/PlanSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,41 @@ export function PlanSettings() {
</For>
</ul>

<section class={styles.cutoffSection}>
<label>
Target days:
<input
type="number"
min={1}
value={api.targetDays()}
onInput={(e) => api.setTargetDays(+e.target.value)}
/>
</label>
<label>
Cutoff (days):
<input
type="number"
min={1}
value={api.cutoffDays() ?? ''}
onInput={(e) => {
const v = e.target.value
api.setCutoffDays(v === '' ? null : +v)
}}
/>
</label>
<label>
Cutoff (date):
<input
type="date"
value={api.cutoffDate() ?? ''}
onInput={(e) => {
const v = e.target.value
api.setCutoffDate(v === '' ? null : v)
}}
/>
</label>
</section>

<section class={styles.dataSection}>
<h2>Data</h2>
<div class={styles.dataButtons}>
Expand Down
25 changes: 23 additions & 2 deletions web/src/components/TagSelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<div class={styles.TagSelector}>
<input type="number" />
<input
type="number"
value={props.value.count}
min={1}
onInput={(e) => props.onChange({ ...props.value, count: +e.target.value })}
/>
<div>
<ul class={styles.tagList}>
<For each={props.value.tags}>{(tagName) => <li>{tagName}</li>}</For>
<For each={props.value.tags}>
{(tagName) => (
<li>
{tagName}
<button type="button" onClick={onRemove(tagName)}>×</button>
</li>
)}
</For>
<li>
<input
class={styles.addTag}
Expand Down
84 changes: 70 additions & 14 deletions web/src/data/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,18 +44,26 @@ export class Api {
this._tagsData = tagsData

// perDayTagData signal
const [perDayTagData, setPerDayTagData] = createSignal<PerDayTagData[]>([
{
tags: ['OT' as Tag],
count: 3,
},
{
tags: ['NT' as Tag],
count: 2,
},
])
const [perDayTagData, setPerDayTagData] = createSignal<PerDayTagData[]>(
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<number | null>(settingsData.cutoffDays)
this.cutoffDays = cutoffDays
this._setCutoffDays = setCutoffDays

// cutoffDate signal
const [cutoffDate, setCutoffDate] = createSignal<string | null>(settingsData.cutoffDate)
this.cutoffDate = cutoffDate
this._setCutoffDate = setCutoffDate

// searchText signal
const [searchText, setSearchText] = createSignal('')
Expand All @@ -80,15 +88,38 @@ export class Api {
private readonly _settingsData: SettingsData
private readonly _tagsData: TagRecord
private readonly _setShowCompleted: Setter<boolean>
private readonly _setTargetDays: Setter<number>
private readonly _setCutoffDays: Setter<number | null>
private readonly _setCutoffDate: Setter<string | null>
private readonly _setPerDayTagData: Setter<PerDayTagData[]>
private readonly _setTimeStampMap: Setter<TimeStampMap>

readonly perDayTagData: Accessor<PerDayTagData[]>
readonly setPerDayTagData: Setter<PerDayTagData[]>
readonly targetDays: Accessor<number>
readonly cutoffDays: Accessor<number | null>
readonly cutoffDate: Accessor<string | null>
readonly searchText: Accessor<string>
readonly setSearchText: Setter<string>
readonly showCompleted: Accessor<boolean>
readonly timeStampMap: Accessor<TimeStampMap>

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
}
Expand Down Expand Up @@ -139,7 +170,10 @@ export class Api {
abbrev,
number,
}: Pick<ChapterData, 'abbrev' | 'number'>) => {
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 => {
Expand Down Expand Up @@ -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 => {
Expand Down
17 changes: 15 additions & 2 deletions web/src/data/indexDb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ import type {
BookAbbrev,
ChapterID,
ISODateTimeString,
PerDayTagData,
SettingsData,
Tag,
TimeStampMap,
} from './model'

Expand All @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -157,6 +163,13 @@ export async function getSettingsData(db: IDBDatabase): Promise<SettingsData> {
const result = (event.target as IDBRequest<SettingsRecord>).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 },
],
})
}

Expand Down
4 changes: 4 additions & 0 deletions web/src/data/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ export type TagRecord = Record<Tag, Record<BookAbbrev, boolean>>

export interface SettingsData {
showCompleted: boolean
targetDays: number
cutoffDays: number | null
cutoffDate: string | null
perDayTagData: PerDayTagData[]
}

export interface PerDayTagData {
Expand Down
49 changes: 15 additions & 34 deletions web/src/utils/groupUtils.ts
Original file line number Diff line number Diff line change
@@ -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[],
Expand All @@ -15,45 +14,27 @@ export function groupByBook(
export function groupByDay(
chapters: ChapterData[],
tagRecord: TagRecord,
tagPerDay: Record<Tag, number>,
perDayTagData: PerDayTagData[],
targetDays: number,
): Record<string, ChapterData[]> {
console.log('[groupByDay]', { chapters, tagRecord, tagPerDay })
const groups: Record<string, ChapterData[]> = {}

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<Tag, number> = {}
const tagChapters: Record<Tag, ChapterData[]> = {}

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
Expand Down