feat: remove calendar and refine habits
ci / docker (push) Successful in 3m53s

This commit is contained in:
2026-09-06 07:56:23 +08:00
parent ceaaaa593e
commit 35e562026c
16 changed files with 277 additions and 500 deletions
+22 -7
View File
@@ -1,16 +1,31 @@
import { describe, expect, it } from 'vitest'
import { dateKey, isTaskView, moveDueDate } from './mvp-utils'
import { dateKey, habitWeek, isHabitComplete, isTaskView, numericHabitInputValue } from './mvp-utils'
describe('MVP view utilities', () => {
it('normalizes to UTC so stored due_at stays stable in every local timezone', () => {
const moved = moveDueDate('2026-09-05T14:30:00+08:00', '2026-09-09')
expect(moved).toBe('2026-09-09T14:30:00.000Z')
const newly = moveDueDate(null, '2026-09-09')
expect(newly).toBe('2026-09-09T09:00:00.000Z')
it('formats a local date as YYYY-MM-DD', () => {
expect(dateKey(new Date(2026, 8, 5))).toBe('2026-09-05')
})
it('returns the monday-to-sunday week containing the requested day', () => {
expect(habitWeek(new Date(2026, 8, 5)).map(dateKey)).toEqual([
'2026-08-31', '2026-09-01', '2026-09-02', '2026-09-03', '2026-09-04', '2026-09-05', '2026-09-06',
])
})
it('identifies only task-backed views as task data loaders', () => {
expect(['tasks', 'today', 'upcoming'].filter(isTaskView)).toEqual(['tasks', 'today', 'upcoming'])
expect(['calendar', 'habits', 'settings', 'trash'].filter(isTaskView)).toEqual([])
expect(['habits', 'settings', 'trash'].filter(isTaskView)).toEqual([])
})
it('only completes numeric habits when progress reaches the target', () => {
expect(isHabitComplete('numeric', 3, 5)).toBe(false)
expect(isHabitComplete('numeric', 5, 5)).toBe(true)
expect(isHabitComplete('boolean', 1, 5)).toBe(true)
})
it('does not invent a numeric value when the input is empty', () => {
expect(numericHabitInputValue(undefined)).toBeNull()
expect(numericHabitInputValue('')).toBeNull()
expect(numericHabitInputValue(4)).toBe(4)
})
})
+11 -20
View File
@@ -5,26 +5,6 @@ export function dateKey(date: Date) {
return `${y}-${m}-${d}`
}
export function calendarRange(date: Date) {
const first = new Date(date.getFullYear(), date.getMonth(), 1)
const from = new Date(first)
const mondayOffset = (first.getDay() + 6) % 7
from.setDate(first.getDate() - mondayOffset)
const to = new Date(from)
to.setDate(from.getDate() + 41)
return { from: dateKey(from), to: dateKey(to) }
}
export function moveDueDate(current: string | null, day: string) {
const source = current ? new Date(current) : null
const hours = source && !Number.isNaN(source.valueOf()) ? source.getHours() : 9
const minutes = source && !Number.isNaN(source.valueOf()) ? source.getMinutes() : 0
const date = new Date(`${day}T00:00:00`)
date.setHours(hours, minutes, 0, 0)
const offsetMs = date.getTimezoneOffset() * 60_000
return new Date(date.getTime() - offsetMs).toISOString()
}
export function habitWeek(now = new Date()) {
const monday = new Date(now.getFullYear(), now.getMonth(), now.getDate())
monday.setDate(monday.getDate() - ((monday.getDay() + 6) % 7))
@@ -43,3 +23,14 @@ export function mergePage<T>(page: T[] | { items?: T[]; next_cursor?: string | n
export function isTaskView(view: string) {
return view === 'tasks' || view === 'today' || view === 'upcoming'
}
export function isHabitComplete(kind: string | undefined, value: number | boolean | undefined, target = 1) {
if (kind === 'numeric') return Number(value ?? 0) >= target
return Boolean(value)
}
export function numericHabitInputValue(input: number | string | undefined) {
if (input === undefined || input === '') return null
const value = Number(input)
return Number.isFinite(value) && value >= 0 ? value : null
}