@even-toolkit/create-even-app 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +159 -0
- package/package.json +28 -0
- package/templates/chat/README.md +27 -0
- package/templates/chat/index.html +12 -0
- package/templates/chat/package.json +34 -0
- package/templates/chat/src/App.tsx +61 -0
- package/templates/chat/src/app.css +54 -0
- package/templates/chat/src/contexts/ChatContext.tsx +99 -0
- package/templates/chat/src/glass/AppGlasses.tsx +70 -0
- package/templates/chat/src/glass/screens/home.ts +24 -0
- package/templates/chat/src/glass/selectors.ts +9 -0
- package/templates/chat/src/glass/shared.ts +8 -0
- package/templates/chat/src/glass/splash.ts +25 -0
- package/templates/chat/src/main.tsx +13 -0
- package/templates/chat/src/screens/ChatScreen.tsx +69 -0
- package/templates/chat/src/screens/Settings.tsx +88 -0
- package/templates/chat/src/types.ts +13 -0
- package/templates/chat/src/vite-env.d.ts +1 -0
- package/templates/chat/template.json +7 -0
- package/templates/chat/tsconfig.json +20 -0
- package/templates/chat/tsconfig.node.json +13 -0
- package/templates/chat/vite.config.ts +12 -0
- package/templates/dashboard/README.md +17 -0
- package/templates/dashboard/index.html +12 -0
- package/templates/dashboard/package.json +34 -0
- package/templates/dashboard/src/App.tsx +27 -0
- package/templates/dashboard/src/app.css +54 -0
- package/templates/dashboard/src/glass/AppGlasses.tsx +53 -0
- package/templates/dashboard/src/glass/screens/home.ts +23 -0
- package/templates/dashboard/src/glass/selectors.ts +9 -0
- package/templates/dashboard/src/glass/shared.ts +8 -0
- package/templates/dashboard/src/glass/splash.ts +22 -0
- package/templates/dashboard/src/main.tsx +13 -0
- package/templates/dashboard/src/screens/ChartsScreen.tsx +99 -0
- package/templates/dashboard/src/screens/OverviewScreen.tsx +102 -0
- package/templates/dashboard/src/screens/SettingsScreen.tsx +60 -0
- package/templates/dashboard/src/vite-env.d.ts +1 -0
- package/templates/dashboard/template.json +7 -0
- package/templates/dashboard/tsconfig.json +20 -0
- package/templates/dashboard/tsconfig.node.json +13 -0
- package/templates/dashboard/vite.config.ts +12 -0
- package/templates/media/README.md +27 -0
- package/templates/media/index.html +12 -0
- package/templates/media/package.json +34 -0
- package/templates/media/src/App.tsx +24 -0
- package/templates/media/src/app.css +54 -0
- package/templates/media/src/contexts/MediaContext.tsx +108 -0
- package/templates/media/src/glass/AppGlasses.tsx +59 -0
- package/templates/media/src/glass/screens/home.ts +24 -0
- package/templates/media/src/glass/selectors.ts +9 -0
- package/templates/media/src/glass/shared.ts +8 -0
- package/templates/media/src/glass/splash.ts +25 -0
- package/templates/media/src/layouts/shell.tsx +39 -0
- package/templates/media/src/main.tsx +13 -0
- package/templates/media/src/screens/AudioScreen.tsx +78 -0
- package/templates/media/src/screens/GalleryScreen.tsx +98 -0
- package/templates/media/src/screens/Settings.tsx +86 -0
- package/templates/media/src/screens/UploadScreen.tsx +95 -0
- package/templates/media/src/types.ts +29 -0
- package/templates/media/src/vite-env.d.ts +1 -0
- package/templates/media/template.json +7 -0
- package/templates/media/tsconfig.json +20 -0
- package/templates/media/tsconfig.node.json +13 -0
- package/templates/media/vite.config.ts +12 -0
- package/templates/minimal/README.md +27 -0
- package/templates/minimal/index.html +12 -0
- package/templates/minimal/package.json +34 -0
- package/templates/minimal/src/App.tsx +50 -0
- package/templates/minimal/src/app.css +54 -0
- package/templates/minimal/src/glass/AppGlasses.tsx +54 -0
- package/templates/minimal/src/glass/screens/home.ts +24 -0
- package/templates/minimal/src/glass/selectors.ts +9 -0
- package/templates/minimal/src/glass/shared.ts +8 -0
- package/templates/minimal/src/glass/splash.ts +25 -0
- package/templates/minimal/src/main.tsx +13 -0
- package/templates/minimal/src/vite-env.d.ts +1 -0
- package/templates/minimal/template.json +7 -0
- package/templates/minimal/tsconfig.json +20 -0
- package/templates/minimal/tsconfig.node.json +13 -0
- package/templates/minimal/vite.config.ts +12 -0
- package/templates/notes/README.md +27 -0
- package/templates/notes/index.html +12 -0
- package/templates/notes/package.json +34 -0
- package/templates/notes/src/App.tsx +25 -0
- package/templates/notes/src/app.css +54 -0
- package/templates/notes/src/contexts/NotesContext.tsx +140 -0
- package/templates/notes/src/glass/AppGlasses.tsx +58 -0
- package/templates/notes/src/glass/screens/home.ts +24 -0
- package/templates/notes/src/glass/selectors.ts +9 -0
- package/templates/notes/src/glass/shared.ts +8 -0
- package/templates/notes/src/glass/splash.ts +24 -0
- package/templates/notes/src/layouts/shell.tsx +36 -0
- package/templates/notes/src/main.tsx +13 -0
- package/templates/notes/src/screens/NoteDetail.tsx +104 -0
- package/templates/notes/src/screens/NoteForm.tsx +84 -0
- package/templates/notes/src/screens/NoteList.tsx +108 -0
- package/templates/notes/src/screens/Settings.tsx +88 -0
- package/templates/notes/src/types.ts +14 -0
- package/templates/notes/src/vite-env.d.ts +1 -0
- package/templates/notes/template.json +7 -0
- package/templates/notes/tsconfig.json +20 -0
- package/templates/notes/tsconfig.node.json +13 -0
- package/templates/notes/vite.config.ts +12 -0
- package/templates/tracker/README.md +27 -0
- package/templates/tracker/index.html +12 -0
- package/templates/tracker/package.json +34 -0
- package/templates/tracker/src/App.tsx +24 -0
- package/templates/tracker/src/app.css +54 -0
- package/templates/tracker/src/contexts/TrackerContext.tsx +193 -0
- package/templates/tracker/src/glass/AppGlasses.tsx +64 -0
- package/templates/tracker/src/glass/screens/home.ts +24 -0
- package/templates/tracker/src/glass/selectors.ts +9 -0
- package/templates/tracker/src/glass/shared.ts +8 -0
- package/templates/tracker/src/glass/splash.ts +24 -0
- package/templates/tracker/src/layouts/shell.tsx +37 -0
- package/templates/tracker/src/main.tsx +13 -0
- package/templates/tracker/src/screens/HistoryScreen.tsx +106 -0
- package/templates/tracker/src/screens/NewEntryScreen.tsx +135 -0
- package/templates/tracker/src/screens/Settings.tsx +135 -0
- package/templates/tracker/src/screens/TodayScreen.tsx +147 -0
- package/templates/tracker/src/types.ts +34 -0
- package/templates/tracker/src/vite-env.d.ts +1 -0
- package/templates/tracker/template.json +7 -0
- package/templates/tracker/tsconfig.json +20 -0
- package/templates/tracker/tsconfig.node.json +13 -0
- package/templates/tracker/vite.config.ts +12 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import { createContext, useContext, useState, useCallback, useEffect, type ReactNode } from 'react'
|
|
2
|
+
import type { TrackerEntry, ActivityType, GoalTargets, DayRecord } from '../types'
|
|
3
|
+
import { DEFAULT_GOALS } from '../types'
|
|
4
|
+
|
|
5
|
+
interface TrackerContextValue {
|
|
6
|
+
entries: TrackerEntry[]
|
|
7
|
+
goals: GoalTargets
|
|
8
|
+
setGoals: (g: GoalTargets) => void
|
|
9
|
+
addEntry: (activity: ActivityType, value: number, note: string) => TrackerEntry
|
|
10
|
+
deleteEntry: (id: string) => void
|
|
11
|
+
getTodayEntries: () => TrackerEntry[]
|
|
12
|
+
getTodayTotal: (activity: ActivityType) => number
|
|
13
|
+
getDayRecords: () => DayRecord[]
|
|
14
|
+
showCompletionBadge: boolean
|
|
15
|
+
setShowCompletionBadge: (v: boolean) => void
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const TrackerContext = createContext<TrackerContextValue | null>(null)
|
|
19
|
+
|
|
20
|
+
const ENTRIES_KEY = '{{APP_NAME}}-entries'
|
|
21
|
+
const GOALS_KEY = '{{APP_NAME}}-goals'
|
|
22
|
+
const SETTINGS_KEY = '{{APP_NAME}}-settings'
|
|
23
|
+
|
|
24
|
+
function generateId(): string {
|
|
25
|
+
return Date.now().toString(36) + Math.random().toString(36).slice(2, 8)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function toDateKey(ts: number): string {
|
|
29
|
+
const d = new Date(ts)
|
|
30
|
+
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function todayKey(): string {
|
|
34
|
+
return toDateKey(Date.now())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateSampleData(): TrackerEntry[] {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
const DAY = 86400000
|
|
40
|
+
const entries: TrackerEntry[] = []
|
|
41
|
+
|
|
42
|
+
for (let d = 6; d >= 0; d--) {
|
|
43
|
+
const dayBase = now - d * DAY
|
|
44
|
+
const dateStart = new Date(dayBase)
|
|
45
|
+
dateStart.setHours(8, 0, 0, 0)
|
|
46
|
+
const base = dateStart.getTime()
|
|
47
|
+
|
|
48
|
+
// Water entries
|
|
49
|
+
const waterCount = d === 0 ? 6 : Math.floor(Math.random() * 4) + 5
|
|
50
|
+
for (let i = 0; i < waterCount; i++) {
|
|
51
|
+
entries.push({
|
|
52
|
+
id: generateId() + `-w${d}-${i}`,
|
|
53
|
+
activity: 'Water',
|
|
54
|
+
value: 1,
|
|
55
|
+
note: '',
|
|
56
|
+
timestamp: base + i * 3600000,
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Steps entry
|
|
61
|
+
const steps = d === 0 ? 7500 : Math.floor(Math.random() * 5000) + 6000
|
|
62
|
+
entries.push({
|
|
63
|
+
id: generateId() + `-s${d}`,
|
|
64
|
+
activity: 'Steps',
|
|
65
|
+
value: steps,
|
|
66
|
+
note: d === 0 ? '' : 'Daily walk',
|
|
67
|
+
timestamp: base + 10 * 3600000,
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
// Focus entry
|
|
71
|
+
const focus = d === 0 ? 45 : Math.floor(Math.random() * 30) + 30
|
|
72
|
+
entries.push({
|
|
73
|
+
id: generateId() + `-f${d}`,
|
|
74
|
+
activity: 'Focus',
|
|
75
|
+
value: focus,
|
|
76
|
+
note: d === 0 ? '' : 'Deep work session',
|
|
77
|
+
timestamp: base + 14 * 3600000,
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return entries
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function loadEntries(): TrackerEntry[] {
|
|
85
|
+
try {
|
|
86
|
+
const raw = localStorage.getItem(ENTRIES_KEY)
|
|
87
|
+
if (raw) {
|
|
88
|
+
const parsed = JSON.parse(raw)
|
|
89
|
+
if (Array.isArray(parsed) && parsed.length > 0) return parsed
|
|
90
|
+
}
|
|
91
|
+
} catch { /* ignore */ }
|
|
92
|
+
return generateSampleData()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function loadGoals(): GoalTargets {
|
|
96
|
+
try {
|
|
97
|
+
const raw = localStorage.getItem(GOALS_KEY)
|
|
98
|
+
if (raw) return JSON.parse(raw)
|
|
99
|
+
} catch { /* ignore */ }
|
|
100
|
+
return DEFAULT_GOALS
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function loadSettings(): { showCompletionBadge: boolean } {
|
|
104
|
+
try {
|
|
105
|
+
const raw = localStorage.getItem(SETTINGS_KEY)
|
|
106
|
+
if (raw) return JSON.parse(raw)
|
|
107
|
+
} catch { /* ignore */ }
|
|
108
|
+
return { showCompletionBadge: true }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function TrackerProvider({ children }: { children: ReactNode }) {
|
|
112
|
+
const [entries, setEntries] = useState<TrackerEntry[]>(loadEntries)
|
|
113
|
+
const [goals, setGoals] = useState<GoalTargets>(loadGoals)
|
|
114
|
+
const [showCompletionBadge, setShowCompletionBadge] = useState(() => loadSettings().showCompletionBadge)
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
localStorage.setItem(ENTRIES_KEY, JSON.stringify(entries))
|
|
118
|
+
}, [entries])
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
localStorage.setItem(GOALS_KEY, JSON.stringify(goals))
|
|
122
|
+
}, [goals])
|
|
123
|
+
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
localStorage.setItem(SETTINGS_KEY, JSON.stringify({ showCompletionBadge }))
|
|
126
|
+
}, [showCompletionBadge])
|
|
127
|
+
|
|
128
|
+
const addEntry = useCallback((activity: ActivityType, value: number, note: string): TrackerEntry => {
|
|
129
|
+
const entry: TrackerEntry = {
|
|
130
|
+
id: generateId(),
|
|
131
|
+
activity,
|
|
132
|
+
value,
|
|
133
|
+
note,
|
|
134
|
+
timestamp: Date.now(),
|
|
135
|
+
}
|
|
136
|
+
setEntries((prev) => [entry, ...prev])
|
|
137
|
+
return entry
|
|
138
|
+
}, [])
|
|
139
|
+
|
|
140
|
+
const deleteEntry = useCallback((id: string) => {
|
|
141
|
+
setEntries((prev) => prev.filter((e) => e.id !== id))
|
|
142
|
+
}, [])
|
|
143
|
+
|
|
144
|
+
const getTodayEntries = useCallback(() => {
|
|
145
|
+
const key = todayKey()
|
|
146
|
+
return entries.filter((e) => toDateKey(e.timestamp) === key)
|
|
147
|
+
}, [entries])
|
|
148
|
+
|
|
149
|
+
const getTodayTotal = useCallback((activity: ActivityType) => {
|
|
150
|
+
const key = todayKey()
|
|
151
|
+
return entries
|
|
152
|
+
.filter((e) => toDateKey(e.timestamp) === key && e.activity === activity)
|
|
153
|
+
.reduce((sum, e) => sum + e.value, 0)
|
|
154
|
+
}, [entries])
|
|
155
|
+
|
|
156
|
+
const getDayRecords = useCallback((): DayRecord[] => {
|
|
157
|
+
const map = new Map<string, TrackerEntry[]>()
|
|
158
|
+
for (const entry of entries) {
|
|
159
|
+
const key = toDateKey(entry.timestamp)
|
|
160
|
+
const existing = map.get(key) ?? []
|
|
161
|
+
existing.push(entry)
|
|
162
|
+
map.set(key, existing)
|
|
163
|
+
}
|
|
164
|
+
return Array.from(map.entries())
|
|
165
|
+
.map(([date, dayEntries]) => ({ date, entries: dayEntries }))
|
|
166
|
+
.sort((a, b) => b.date.localeCompare(a.date))
|
|
167
|
+
}, [entries])
|
|
168
|
+
|
|
169
|
+
return (
|
|
170
|
+
<TrackerContext.Provider
|
|
171
|
+
value={{
|
|
172
|
+
entries,
|
|
173
|
+
goals,
|
|
174
|
+
setGoals,
|
|
175
|
+
addEntry,
|
|
176
|
+
deleteEntry,
|
|
177
|
+
getTodayEntries,
|
|
178
|
+
getTodayTotal,
|
|
179
|
+
getDayRecords,
|
|
180
|
+
showCompletionBadge,
|
|
181
|
+
setShowCompletionBadge,
|
|
182
|
+
}}
|
|
183
|
+
>
|
|
184
|
+
{children}
|
|
185
|
+
</TrackerContext.Provider>
|
|
186
|
+
)
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function useTracker() {
|
|
190
|
+
const ctx = useContext(TrackerContext)
|
|
191
|
+
if (!ctx) throw new Error('useTracker must be used within TrackerProvider')
|
|
192
|
+
return ctx
|
|
193
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { useCallback, useMemo, useRef } from 'react'
|
|
2
|
+
import { useNavigate, useLocation } from 'react-router'
|
|
3
|
+
import { useGlasses } from 'even-toolkit/useGlasses'
|
|
4
|
+
import { useFlashPhase } from 'even-toolkit/useFlashPhase'
|
|
5
|
+
import { createScreenMapper, getHomeTiles } from 'even-toolkit/glass-router'
|
|
6
|
+
import { appSplash } from './splash'
|
|
7
|
+
import { toDisplayData, onGlassAction, type AppSnapshot } from './selectors'
|
|
8
|
+
import type { AppActions } from './shared'
|
|
9
|
+
import { useTracker } from '../contexts/TrackerContext'
|
|
10
|
+
|
|
11
|
+
const deriveScreen = createScreenMapper([
|
|
12
|
+
{ pattern: '/', screen: 'home' },
|
|
13
|
+
], 'home')
|
|
14
|
+
|
|
15
|
+
const homeTiles = getHomeTiles(appSplash)
|
|
16
|
+
|
|
17
|
+
export function AppGlasses() {
|
|
18
|
+
const navigate = useNavigate()
|
|
19
|
+
const location = useLocation()
|
|
20
|
+
const { goals, getTodayTotal } = useTracker()
|
|
21
|
+
const flashPhase = useFlashPhase(deriveScreen(location.pathname) === 'home')
|
|
22
|
+
|
|
23
|
+
const snapshotRef = useMemo(() => ({
|
|
24
|
+
current: null as AppSnapshot | null,
|
|
25
|
+
}), [])
|
|
26
|
+
|
|
27
|
+
const waterTotal = getTodayTotal('Water')
|
|
28
|
+
const stepsTotal = getTodayTotal('Steps')
|
|
29
|
+
const focusTotal = getTodayTotal('Focus')
|
|
30
|
+
|
|
31
|
+
const snapshot: AppSnapshot = {
|
|
32
|
+
items: [
|
|
33
|
+
`Water: ${waterTotal}/${goals.water}`,
|
|
34
|
+
`Steps: ${stepsTotal.toLocaleString()}/${goals.steps.toLocaleString()}`,
|
|
35
|
+
`Focus: ${focusTotal}/${goals.focus} min`,
|
|
36
|
+
],
|
|
37
|
+
flashPhase,
|
|
38
|
+
}
|
|
39
|
+
snapshotRef.current = snapshot
|
|
40
|
+
|
|
41
|
+
const getSnapshot = useCallback(() => snapshotRef.current!, [snapshotRef])
|
|
42
|
+
|
|
43
|
+
const ctxRef = useRef<AppActions>({ navigate })
|
|
44
|
+
ctxRef.current = { navigate }
|
|
45
|
+
|
|
46
|
+
const handleGlassAction = useCallback(
|
|
47
|
+
(action: Parameters<typeof onGlassAction>[0], nav: Parameters<typeof onGlassAction>[1], snap: AppSnapshot) =>
|
|
48
|
+
onGlassAction(action, nav, snap, ctxRef.current),
|
|
49
|
+
[],
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
useGlasses({
|
|
53
|
+
getSnapshot,
|
|
54
|
+
toDisplayData,
|
|
55
|
+
onGlassAction: handleGlassAction,
|
|
56
|
+
deriveScreen,
|
|
57
|
+
appName: '{{DISPLAY_NAME_UPPER}}',
|
|
58
|
+
splash: appSplash,
|
|
59
|
+
getPageMode: (screen) => screen === 'home' ? 'home' : 'text',
|
|
60
|
+
homeImageTiles: homeTiles,
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
return null
|
|
64
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { GlassScreen } from 'even-toolkit/glass-screen-router'
|
|
2
|
+
import { buildScrollableList } from 'even-toolkit/glass-display-builders'
|
|
3
|
+
import { moveHighlight } from 'even-toolkit/glass-nav'
|
|
4
|
+
import type { AppSnapshot, AppActions } from '../shared'
|
|
5
|
+
|
|
6
|
+
export const homeScreen: GlassScreen<AppSnapshot, AppActions> = {
|
|
7
|
+
display(snapshot, nav) {
|
|
8
|
+
return {
|
|
9
|
+
lines: buildScrollableList({
|
|
10
|
+
items: snapshot.items,
|
|
11
|
+
highlightedIndex: nav.highlightedIndex,
|
|
12
|
+
maxVisible: 5,
|
|
13
|
+
formatter: (item) => item,
|
|
14
|
+
}),
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
action(action, nav, snapshot) {
|
|
19
|
+
if (action.type === 'HIGHLIGHT_MOVE') {
|
|
20
|
+
return { ...nav, highlightedIndex: moveHighlight(nav.highlightedIndex, action.direction, snapshot.items.length - 1) }
|
|
21
|
+
}
|
|
22
|
+
return nav
|
|
23
|
+
},
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createGlassScreenRouter } from 'even-toolkit/glass-screen-router'
|
|
2
|
+
import type { AppSnapshot, AppActions } from './shared'
|
|
3
|
+
import { homeScreen } from './screens/home'
|
|
4
|
+
|
|
5
|
+
export type { AppSnapshot, AppActions }
|
|
6
|
+
|
|
7
|
+
export const { toDisplayData, onGlassAction } = createGlassScreenRouter<AppSnapshot, AppActions>({
|
|
8
|
+
'home': homeScreen,
|
|
9
|
+
}, 'home')
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { createSplash, TILE_PRESETS } from 'even-toolkit/splash'
|
|
2
|
+
|
|
3
|
+
export function renderSplash(ctx: CanvasRenderingContext2D, w: number, h: number) {
|
|
4
|
+
const fg = '#e0e0e0'
|
|
5
|
+
const cx = w / 2
|
|
6
|
+
const s = Math.min(w / 200, h / 200)
|
|
7
|
+
|
|
8
|
+
ctx.fillStyle = fg
|
|
9
|
+
ctx.font = `bold ${14 * s}px "Courier New", monospace`
|
|
10
|
+
ctx.textAlign = 'center'
|
|
11
|
+
ctx.fillText('{{DISPLAY_NAME_UPPER}}', cx, 50 * s)
|
|
12
|
+
ctx.textAlign = 'left'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const appSplash = createSplash({
|
|
16
|
+
tiles: 1,
|
|
17
|
+
tileLayout: 'vertical',
|
|
18
|
+
tilePositions: TILE_PRESETS.topCenter1,
|
|
19
|
+
canvasSize: { w: 200, h: 200 },
|
|
20
|
+
minTimeMs: 0,
|
|
21
|
+
maxTimeMs: 0,
|
|
22
|
+
menuText: '',
|
|
23
|
+
render: renderSplash,
|
|
24
|
+
})
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { DrawerShell } from 'even-toolkit/web'
|
|
2
|
+
import type { SideDrawerItem } from 'even-toolkit/web'
|
|
3
|
+
|
|
4
|
+
const MENU_ITEMS: SideDrawerItem[] = [
|
|
5
|
+
{ id: '/', label: 'Today', section: 'Tracker' },
|
|
6
|
+
{ id: '/history', label: 'History', section: 'Tracker' },
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
const BOTTOM_ITEMS: SideDrawerItem[] = [
|
|
10
|
+
{ id: '/settings', label: 'Settings', section: 'App' },
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
function getPageTitle(pathname: string): string {
|
|
14
|
+
if (pathname === '/') return '{{DISPLAY_NAME}}'
|
|
15
|
+
if (pathname === '/history') return 'History'
|
|
16
|
+
if (pathname === '/new') return 'New Entry'
|
|
17
|
+
if (pathname === '/settings') return 'Settings'
|
|
18
|
+
return '{{DISPLAY_NAME}}'
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function deriveActiveId(pathname: string): string {
|
|
22
|
+
if (pathname === '/settings') return '/settings'
|
|
23
|
+
if (pathname === '/history') return '/history'
|
|
24
|
+
return '/'
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function Shell() {
|
|
28
|
+
return (
|
|
29
|
+
<DrawerShell
|
|
30
|
+
items={MENU_ITEMS}
|
|
31
|
+
bottomItems={BOTTOM_ITEMS}
|
|
32
|
+
title="{{DISPLAY_NAME}}"
|
|
33
|
+
getPageTitle={getPageTitle}
|
|
34
|
+
deriveActiveId={deriveActiveId}
|
|
35
|
+
/>
|
|
36
|
+
)
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { StrictMode } from 'react'
|
|
2
|
+
import { createRoot } from 'react-dom/client'
|
|
3
|
+
import { BrowserRouter } from 'react-router'
|
|
4
|
+
import { App } from './App'
|
|
5
|
+
import './app.css'
|
|
6
|
+
|
|
7
|
+
createRoot(document.getElementById('root')!).render(
|
|
8
|
+
<StrictMode>
|
|
9
|
+
<BrowserRouter>
|
|
10
|
+
<App />
|
|
11
|
+
</BrowserRouter>
|
|
12
|
+
</StrictMode>,
|
|
13
|
+
)
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useMemo } from 'react'
|
|
2
|
+
import { Card, Calendar, Timeline, Badge, ScreenHeader } from 'even-toolkit/web'
|
|
3
|
+
import type { CalendarEvent } from 'even-toolkit/web'
|
|
4
|
+
import { useTracker } from '../contexts/TrackerContext'
|
|
5
|
+
import type { DayRecord } from '../types'
|
|
6
|
+
|
|
7
|
+
const ACTIVITY_COLORS: Record<string, string> = {
|
|
8
|
+
Water: 'var(--color-accent)',
|
|
9
|
+
Steps: 'var(--color-positive)',
|
|
10
|
+
Focus: 'var(--color-negative)',
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function formatDayLabel(dateStr: string): string {
|
|
14
|
+
const d = new Date(dateStr + 'T12:00:00')
|
|
15
|
+
const today = new Date()
|
|
16
|
+
const yesterday = new Date()
|
|
17
|
+
yesterday.setDate(yesterday.getDate() - 1)
|
|
18
|
+
|
|
19
|
+
if (d.toDateString() === today.toDateString()) return 'Today'
|
|
20
|
+
if (d.toDateString() === yesterday.toDateString()) return 'Yesterday'
|
|
21
|
+
return d.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getDayCompletion(record: DayRecord, goals: { water: number; steps: number; focus: number }): number {
|
|
25
|
+
const waterTotal = record.entries.filter((e) => e.activity === 'Water').reduce((s, e) => s + e.value, 0)
|
|
26
|
+
const stepsTotal = record.entries.filter((e) => e.activity === 'Steps').reduce((s, e) => s + e.value, 0)
|
|
27
|
+
const focusTotal = record.entries.filter((e) => e.activity === 'Focus').reduce((s, e) => s + e.value, 0)
|
|
28
|
+
|
|
29
|
+
const waterPct = Math.min(waterTotal / goals.water, 1)
|
|
30
|
+
const stepsPct = Math.min(stepsTotal / goals.steps, 1)
|
|
31
|
+
const focusPct = Math.min(focusTotal / goals.focus, 1)
|
|
32
|
+
|
|
33
|
+
return Math.round(((waterPct + stepsPct + focusPct) / 3) * 100)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function HistoryScreen() {
|
|
37
|
+
const { getDayRecords, goals, showCompletionBadge } = useTracker()
|
|
38
|
+
const dayRecords = getDayRecords()
|
|
39
|
+
|
|
40
|
+
// Calendar events from entries
|
|
41
|
+
const calendarEvents: CalendarEvent[] = useMemo(() => {
|
|
42
|
+
const events: CalendarEvent[] = []
|
|
43
|
+
for (const record of dayRecords) {
|
|
44
|
+
const activities = new Set(record.entries.map((e) => e.activity))
|
|
45
|
+
for (const activity of activities) {
|
|
46
|
+
const d = new Date(record.date + 'T12:00:00')
|
|
47
|
+
events.push({
|
|
48
|
+
id: `${record.date}-${activity}`,
|
|
49
|
+
title: activity,
|
|
50
|
+
start: d,
|
|
51
|
+
end: d,
|
|
52
|
+
color: ACTIVITY_COLORS[activity],
|
|
53
|
+
})
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return events
|
|
57
|
+
}, [dayRecords])
|
|
58
|
+
|
|
59
|
+
// Recent 7 days for timeline
|
|
60
|
+
const recentDays = dayRecords.slice(0, 7)
|
|
61
|
+
|
|
62
|
+
const timelineEvents = useMemo(() => {
|
|
63
|
+
return recentDays.map((record) => {
|
|
64
|
+
const completion = getDayCompletion(record, goals)
|
|
65
|
+
const entryCount = record.entries.length
|
|
66
|
+
return {
|
|
67
|
+
id: record.date,
|
|
68
|
+
title: formatDayLabel(record.date),
|
|
69
|
+
subtitle: `${entryCount} ${entryCount === 1 ? 'entry' : 'entries'}`,
|
|
70
|
+
timestamp: `${completion}%`,
|
|
71
|
+
color: completion >= 80 ? 'var(--color-positive)' : completion >= 50 ? 'var(--color-accent)' : 'var(--color-negative)',
|
|
72
|
+
}
|
|
73
|
+
})
|
|
74
|
+
}, [recentDays, goals])
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<main className="px-3 pt-4 pb-8 space-y-3">
|
|
78
|
+
<ScreenHeader
|
|
79
|
+
title="History"
|
|
80
|
+
subtitle={`${dayRecords.length} days tracked`}
|
|
81
|
+
/>
|
|
82
|
+
|
|
83
|
+
{/* Calendar */}
|
|
84
|
+
<Card className="p-4">
|
|
85
|
+
<Calendar events={calendarEvents} />
|
|
86
|
+
</Card>
|
|
87
|
+
|
|
88
|
+
{/* Recent Activity Timeline */}
|
|
89
|
+
<Card className="p-4 space-y-3">
|
|
90
|
+
<div className="flex items-center justify-between">
|
|
91
|
+
<p className="text-[15px] tracking-[-0.15px] text-text">Recent Activity</p>
|
|
92
|
+
{showCompletionBadge && recentDays.length > 0 && (
|
|
93
|
+
<Badge variant={getDayCompletion(recentDays[0], goals) >= 80 ? 'positive' : 'neutral'}>
|
|
94
|
+
{getDayCompletion(recentDays[0], goals)}% today
|
|
95
|
+
</Badge>
|
|
96
|
+
)}
|
|
97
|
+
</div>
|
|
98
|
+
{timelineEvents.length > 0 ? (
|
|
99
|
+
<Timeline events={timelineEvents} />
|
|
100
|
+
) : (
|
|
101
|
+
<p className="text-[13px] tracking-[-0.13px] text-text-dim">No activity recorded yet.</p>
|
|
102
|
+
)}
|
|
103
|
+
</Card>
|
|
104
|
+
</main>
|
|
105
|
+
)
|
|
106
|
+
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { useState } from 'react'
|
|
2
|
+
import { useNavigate } from 'react-router'
|
|
3
|
+
import { Card, Button, StepIndicator, Select, Slider, Input, Textarea, useDrawerHeader } from 'even-toolkit/web'
|
|
4
|
+
import { useTracker } from '../contexts/TrackerContext'
|
|
5
|
+
import { ACTIVITIES, ACTIVITY_UNITS, type ActivityType } from '../types'
|
|
6
|
+
|
|
7
|
+
const ACTIVITY_OPTIONS = ACTIVITIES.map((a) => ({ value: a, label: a }))
|
|
8
|
+
|
|
9
|
+
const ACTIVITY_DEFAULTS: Record<ActivityType, { min: number; max: number; step: number; initial: number }> = {
|
|
10
|
+
Water: { min: 1, max: 12, step: 1, initial: 1 },
|
|
11
|
+
Steps: { min: 500, max: 20000, step: 500, initial: 5000 },
|
|
12
|
+
Focus: { min: 5, max: 120, step: 5, initial: 25 },
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function NewEntryScreen() {
|
|
16
|
+
const navigate = useNavigate()
|
|
17
|
+
const { addEntry } = useTracker()
|
|
18
|
+
|
|
19
|
+
const [step, setStep] = useState(1)
|
|
20
|
+
const [activity, setActivity] = useState<ActivityType>('Water')
|
|
21
|
+
const [value, setValue] = useState(ACTIVITY_DEFAULTS.Water.initial)
|
|
22
|
+
const [note, setNote] = useState('')
|
|
23
|
+
|
|
24
|
+
useDrawerHeader({ title: 'New Entry', backTo: '/' })
|
|
25
|
+
|
|
26
|
+
function handleActivityChange(a: string) {
|
|
27
|
+
const act = a as ActivityType
|
|
28
|
+
setActivity(act)
|
|
29
|
+
setValue(ACTIVITY_DEFAULTS[act].initial)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleSave() {
|
|
33
|
+
addEntry(activity, value, note.trim())
|
|
34
|
+
navigate('/')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const canNext = step < 3
|
|
38
|
+
const canPrev = step > 1
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<main className="px-3 pt-4 pb-8 space-y-3">
|
|
42
|
+
<StepIndicator
|
|
43
|
+
currentStep={step}
|
|
44
|
+
totalSteps={3}
|
|
45
|
+
onPrev={canPrev ? () => setStep((s) => s - 1) : undefined}
|
|
46
|
+
onNext={
|
|
47
|
+
step === 3
|
|
48
|
+
? handleSave
|
|
49
|
+
: canNext
|
|
50
|
+
? () => setStep((s) => s + 1)
|
|
51
|
+
: undefined
|
|
52
|
+
}
|
|
53
|
+
nextLabel={step === 3 ? 'Save' : undefined}
|
|
54
|
+
/>
|
|
55
|
+
|
|
56
|
+
{/* Step 1: Select Activity */}
|
|
57
|
+
{step === 1 && (
|
|
58
|
+
<Card className="p-4 space-y-3">
|
|
59
|
+
<p className="text-[15px] tracking-[-0.15px] text-text">Select Activity</p>
|
|
60
|
+
<div className="space-y-1.5">
|
|
61
|
+
<label className="text-[11px] tracking-[-0.11px] text-text-dim block">Activity type</label>
|
|
62
|
+
<Select
|
|
63
|
+
options={ACTIVITY_OPTIONS}
|
|
64
|
+
value={activity}
|
|
65
|
+
onValueChange={handleActivityChange}
|
|
66
|
+
/>
|
|
67
|
+
</div>
|
|
68
|
+
<p className="text-[13px] tracking-[-0.13px] text-text-dim">
|
|
69
|
+
Track your {activity.toLowerCase()} intake for today.
|
|
70
|
+
</p>
|
|
71
|
+
</Card>
|
|
72
|
+
)}
|
|
73
|
+
|
|
74
|
+
{/* Step 2: Set Value */}
|
|
75
|
+
{step === 2 && (
|
|
76
|
+
<Card className="p-4 space-y-3">
|
|
77
|
+
<p className="text-[15px] tracking-[-0.15px] text-text">Set Value</p>
|
|
78
|
+
<div className="space-y-1.5">
|
|
79
|
+
<label className="text-[11px] tracking-[-0.11px] text-text-dim block">
|
|
80
|
+
{activity} ({ACTIVITY_UNITS[activity]})
|
|
81
|
+
</label>
|
|
82
|
+
<Slider
|
|
83
|
+
value={value}
|
|
84
|
+
onChange={setValue}
|
|
85
|
+
min={ACTIVITY_DEFAULTS[activity].min}
|
|
86
|
+
max={ACTIVITY_DEFAULTS[activity].max}
|
|
87
|
+
step={ACTIVITY_DEFAULTS[activity].step}
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="space-y-1.5">
|
|
91
|
+
<label className="text-[11px] tracking-[-0.11px] text-text-dim block">Exact value</label>
|
|
92
|
+
<Input
|
|
93
|
+
type="number"
|
|
94
|
+
value={String(value)}
|
|
95
|
+
onChange={(e) => {
|
|
96
|
+
const v = Number(e.target.value)
|
|
97
|
+
if (!isNaN(v) && v >= 0) setValue(v)
|
|
98
|
+
}}
|
|
99
|
+
/>
|
|
100
|
+
</div>
|
|
101
|
+
<p className="text-[13px] tracking-[-0.13px] text-text-dim">
|
|
102
|
+
{value} {ACTIVITY_UNITS[activity]}
|
|
103
|
+
</p>
|
|
104
|
+
</Card>
|
|
105
|
+
)}
|
|
106
|
+
|
|
107
|
+
{/* Step 3: Add Note */}
|
|
108
|
+
{step === 3 && (
|
|
109
|
+
<Card className="p-4 space-y-3">
|
|
110
|
+
<p className="text-[15px] tracking-[-0.15px] text-text">Add Note</p>
|
|
111
|
+
<div className="space-y-1.5">
|
|
112
|
+
<label className="text-[11px] tracking-[-0.11px] text-text-dim block">Note (optional)</label>
|
|
113
|
+
<Textarea
|
|
114
|
+
value={note}
|
|
115
|
+
onChange={(e) => setNote(e.target.value)}
|
|
116
|
+
placeholder="Add a note about this entry..."
|
|
117
|
+
rows={4}
|
|
118
|
+
/>
|
|
119
|
+
</div>
|
|
120
|
+
<div className="rounded-[6px] bg-surface p-3 space-y-1">
|
|
121
|
+
<p className="text-[13px] tracking-[-0.13px] text-text">Summary</p>
|
|
122
|
+
<p className="text-[11px] tracking-[-0.11px] text-text-dim">
|
|
123
|
+
{activity}: {value} {ACTIVITY_UNITS[activity]}
|
|
124
|
+
</p>
|
|
125
|
+
{note.trim() && (
|
|
126
|
+
<p className="text-[11px] tracking-[-0.11px] text-text-dim">
|
|
127
|
+
Note: {note.trim()}
|
|
128
|
+
</p>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
</Card>
|
|
132
|
+
)}
|
|
133
|
+
</main>
|
|
134
|
+
)
|
|
135
|
+
}
|