super-time-tracker-ui 0.1.2

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.
Files changed (276) hide show
  1. package/AGENTS.md +5 -0
  2. package/CHANGELOG.md +28 -0
  3. package/CLAUDE.md +1 -0
  4. package/README.md +36 -0
  5. package/app/api/backup/route.ts +39 -0
  6. package/app/api/entry/delete-bulk/route.ts +53 -0
  7. package/app/api/entry/move/route.ts +46 -0
  8. package/app/api/entry/move-bulk/route.ts +62 -0
  9. package/app/api/entry/route.ts +75 -0
  10. package/app/api/in/route.ts +38 -0
  11. package/app/api/note/route.ts +120 -0
  12. package/app/api/out/route.ts +31 -0
  13. package/app/api/sheet/route.ts +68 -0
  14. package/app/api/state/route.ts +16 -0
  15. package/app/api/tags/route.ts +75 -0
  16. package/app/color-palettes.css +260 -0
  17. package/app/favicon.ico +0 -0
  18. package/app/globals.css +140 -0
  19. package/app/layout.tsx +54 -0
  20. package/app/page.tsx +24 -0
  21. package/app/reporting/page.tsx +11 -0
  22. package/app/settings/data/page.tsx +9 -0
  23. package/app/settings/display/page.tsx +8 -0
  24. package/app/settings/page.tsx +12 -0
  25. package/app/settings/tags/page.tsx +13 -0
  26. package/bin/stt-ui.js +63 -0
  27. package/components/active-entry-panel.tsx +199 -0
  28. package/components/backup-restore-setting.tsx +168 -0
  29. package/components/check-in-form-collapsed-setting.tsx +44 -0
  30. package/components/check-in-form-collapsible.tsx +52 -0
  31. package/components/check-in-form.tsx +89 -0
  32. package/components/checkbox.tsx +75 -0
  33. package/components/checkout-button-group.tsx +73 -0
  34. package/components/chevron-icon.tsx +25 -0
  35. package/components/clear-tag-filters-on-sheet-change-setting.tsx +45 -0
  36. package/components/color-palette-setting.tsx +75 -0
  37. package/components/compact-lists-setting.tsx +42 -0
  38. package/components/confirm-before-checkout-setting.tsx +42 -0
  39. package/components/confirm-destructive-actions-setting.tsx +46 -0
  40. package/components/confirm-dialog-provider.tsx +71 -0
  41. package/components/confirm-dialog.tsx +90 -0
  42. package/components/data-settings-view.tsx +47 -0
  43. package/components/default-reporting-range-setting.tsx +56 -0
  44. package/components/default-reporting-sort-setting.tsx +45 -0
  45. package/components/default-sheet-session-setting.tsx +118 -0
  46. package/components/display-settings-view.tsx +75 -0
  47. package/components/duration-format-setting.tsx +40 -0
  48. package/components/entry-actions-menu.tsx +207 -0
  49. package/components/entry-edit-form.tsx +113 -0
  50. package/components/entry-list-bulk-bar.tsx +128 -0
  51. package/components/entry-list-sort-setting.tsx +41 -0
  52. package/components/entry-list.tsx +336 -0
  53. package/components/entry-notes-list.tsx +211 -0
  54. package/components/entry-tag-filter.tsx +99 -0
  55. package/components/format_datetime_hint.ts +8 -0
  56. package/components/format_time.ts +10 -0
  57. package/components/general-settings-view.tsx +40 -0
  58. package/components/hamburger-icon.tsx +21 -0
  59. package/components/note-edit-form.tsx +77 -0
  60. package/components/note-form.tsx +109 -0
  61. package/components/pencil-icon.tsx +21 -0
  62. package/components/reporting-date-range-picker.tsx +121 -0
  63. package/components/reporting-sort-controls.tsx +53 -0
  64. package/components/reporting-view.tsx +340 -0
  65. package/components/setting-radio-group.tsx +79 -0
  66. package/components/settings-nav.tsx +66 -0
  67. package/components/settings-page-layout.tsx +53 -0
  68. package/components/settings-saved-toast.tsx +57 -0
  69. package/components/sheet-actions-menu.tsx +108 -0
  70. package/components/sheet-sidebar.tsx +196 -0
  71. package/components/tag-autocomplete-input.tsx +183 -0
  72. package/components/tag-filter-mode-setting.tsx +47 -0
  73. package/components/tag-management-view.tsx +290 -0
  74. package/components/theme-mode-setting.tsx +44 -0
  75. package/components/theme-mode-system-listener.tsx +43 -0
  76. package/components/theme_switcher.tsx +38 -0
  77. package/components/time-format-setting.tsx +39 -0
  78. package/components/timer-in-title-setting.tsx +38 -0
  79. package/components/timer-show-seconds-setting.tsx +41 -0
  80. package/components/tracker-active-bar.tsx +76 -0
  81. package/components/tracker-app.tsx +338 -0
  82. package/components/tracker-breadcrumb.tsx +56 -0
  83. package/components/tracker-document-title.tsx +67 -0
  84. package/components/tracker-topbar.tsx +63 -0
  85. package/components/trash-icon.tsx +24 -0
  86. package/components/week-starts-on-setting.tsx +39 -0
  87. package/eslint.config.mjs +18 -0
  88. package/lib/add_note_to_entry.ts +65 -0
  89. package/lib/api_error_response.ts +10 -0
  90. package/lib/apply_accent_color.ts +12 -0
  91. package/lib/apply_color_palette.ts +12 -0
  92. package/lib/apply_compact_lists.ts +9 -0
  93. package/lib/apply_tag_autocomplete_selection.ts +26 -0
  94. package/lib/apply_theme.ts +8 -0
  95. package/lib/build_reporting_stats.ts +55 -0
  96. package/lib/build_resume_description.ts +15 -0
  97. package/lib/check_in_entry.ts +81 -0
  98. package/lib/check_out_entry.ts +75 -0
  99. package/lib/collect_known_tags.ts +22 -0
  100. package/lib/collect_tag_stats.ts +27 -0
  101. package/lib/collect_tags_from_entries.ts +35 -0
  102. package/lib/config.ts +9 -0
  103. package/lib/convert_json_db.ts +49 -0
  104. package/lib/delete_entries.ts +62 -0
  105. package/lib/delete_entry.ts +29 -0
  106. package/lib/delete_note_on_entry.ts +42 -0
  107. package/lib/delete_sheet.ts +30 -0
  108. package/lib/delete_tracker_action.ts +22 -0
  109. package/lib/edit_entry.ts +56 -0
  110. package/lib/edit_note_on_entry.ts +49 -0
  111. package/lib/ensure_dir_exists.ts +22 -0
  112. package/lib/entry_matches_tag_filter.ts +26 -0
  113. package/lib/fetch_tracker_state.ts +15 -0
  114. package/lib/filter_entries_by_tags.ts +20 -0
  115. package/lib/filter_known_tags.ts +20 -0
  116. package/lib/find_all_serialized_active_entries.ts +28 -0
  117. package/lib/find_serialized_active_entry.ts +12 -0
  118. package/lib/find_serialized_active_entry_for_sheet.ts +31 -0
  119. package/lib/find_sheet_with_active_entry.ts +16 -0
  120. package/lib/format_display_tag.ts +6 -0
  121. package/lib/format_duration.ts +45 -0
  122. package/lib/gen_db.ts +43 -0
  123. package/lib/get_active_panel_class_name.ts +20 -0
  124. package/lib/get_average_entry_ms.ts +13 -0
  125. package/lib/get_button_class_name.ts +24 -0
  126. package/lib/get_check_out_confirm_dialog.ts +19 -0
  127. package/lib/get_clipped_entry_duration_ms.ts +18 -0
  128. package/lib/get_compact_lists_snapshot.ts +15 -0
  129. package/lib/get_date_range_ms_from_inputs.ts +31 -0
  130. package/lib/get_delete_entries_confirm_dialog.ts +21 -0
  131. package/lib/get_delete_entry_confirm_dialog.ts +19 -0
  132. package/lib/get_delete_note_confirm_dialog.ts +21 -0
  133. package/lib/get_delete_sheet_confirm_dialog.ts +25 -0
  134. package/lib/get_entry_duration_ms.ts +14 -0
  135. package/lib/get_entry_row_key.ts +8 -0
  136. package/lib/get_initial_preferred_sheet_name.ts +34 -0
  137. package/lib/get_initial_reporting_range_inputs.ts +31 -0
  138. package/lib/get_input_class_name.ts +15 -0
  139. package/lib/get_merge_tags_confirm_dialog.ts +25 -0
  140. package/lib/get_period_range_ms.ts +43 -0
  141. package/lib/get_reporting_date_range_shortcut_inputs.ts +84 -0
  142. package/lib/get_reporting_period_totals.ts +39 -0
  143. package/lib/get_reporting_stats.ts +25 -0
  144. package/lib/get_restore_db_confirm_dialog.ts +14 -0
  145. package/lib/get_running_entry_key.ts +8 -0
  146. package/lib/get_serialized_entries_total_ms.ts +10 -0
  147. package/lib/get_sheet.ts +14 -0
  148. package/lib/get_sheet_report_stats.ts +22 -0
  149. package/lib/get_sheet_report_stats_for_range.ts +46 -0
  150. package/lib/get_sheet_tag_filter_snapshot.ts +22 -0
  151. package/lib/get_sheets_duration_in_range.ts +27 -0
  152. package/lib/get_tag_autocomplete_context.ts +32 -0
  153. package/lib/get_theme_snapshot.ts +16 -0
  154. package/lib/get_tracker_state.ts +67 -0
  155. package/lib/has_string_value.ts +6 -0
  156. package/lib/is_entry_in_day.ts +15 -0
  157. package/lib/is_idle_sheet_report.ts +8 -0
  158. package/lib/is_json_time_tracker_db.ts +14 -0
  159. package/lib/merge_tags_across_db.ts +79 -0
  160. package/lib/migrate_json_db.ts +56 -0
  161. package/lib/migrate_json_db_to_version_three.ts +51 -0
  162. package/lib/migrate_json_db_to_version_two.ts +50 -0
  163. package/lib/move_entries_to_sheet.ts +152 -0
  164. package/lib/move_entry_to_sheet.ts +82 -0
  165. package/lib/normalize_stored_tag.ts +16 -0
  166. package/lib/notify_settings_saved.ts +47 -0
  167. package/lib/parse_default_sheet_session_mode.ts +21 -0
  168. package/lib/parse_entry_from_input.ts +23 -0
  169. package/lib/parse_natural_language_date.ts +23 -0
  170. package/lib/parse_reporting_source_sheets.ts +22 -0
  171. package/lib/partition_sheet_report_stats.ts +30 -0
  172. package/lib/patch_tracker_action.ts +22 -0
  173. package/lib/persist_ui_preference.ts +18 -0
  174. package/lib/post_tracker_action.ts +22 -0
  175. package/lib/preferences/accent_color_preference.ts +21 -0
  176. package/lib/preferences/check_in_form_collapsed_preference.ts +20 -0
  177. package/lib/preferences/clear_tag_filters_on_sheet_change_preference.ts +20 -0
  178. package/lib/preferences/color_palette_preference.ts +21 -0
  179. package/lib/preferences/confirm_before_checkout_preference.ts +20 -0
  180. package/lib/preferences/confirm_destructive_actions_preference.ts +20 -0
  181. package/lib/preferences/default_reporting_range_preference.ts +21 -0
  182. package/lib/preferences/default_reporting_sort_preference.ts +24 -0
  183. package/lib/preferences/duration_format_preference.ts +19 -0
  184. package/lib/preferences/entry_list_sort_preference.ts +21 -0
  185. package/lib/preferences/tag_filter_mode_preference.ts +18 -0
  186. package/lib/preferences/theme_mode_preference.ts +18 -0
  187. package/lib/preferences/time_format_preference.ts +18 -0
  188. package/lib/preferences/timer_in_title_preference.ts +18 -0
  189. package/lib/preferences/timer_show_seconds_preference.ts +19 -0
  190. package/lib/preferences/week_starts_on_preference.ts +19 -0
  191. package/lib/prompt_check_out_at.ts +17 -0
  192. package/lib/prompt_entry_note.ts +14 -0
  193. package/lib/prune_sheet_tag_filter.ts +27 -0
  194. package/lib/read_db.ts +49 -0
  195. package/lib/read_db_backup_contents.ts +22 -0
  196. package/lib/read_document_compact_lists.ts +12 -0
  197. package/lib/read_document_theme.ts +14 -0
  198. package/lib/read_sheet_tag_filter.ts +26 -0
  199. package/lib/read_stored_active_sheet.ts +14 -0
  200. package/lib/read_stored_compact_lists.ts +24 -0
  201. package/lib/read_stored_default_sheet_fixed_name.ts +16 -0
  202. package/lib/read_stored_default_sheet_session_mode.ts +18 -0
  203. package/lib/read_stored_sheet_tag_filters.ts +28 -0
  204. package/lib/read_stored_theme.ts +18 -0
  205. package/lib/rename_sheet.ts +39 -0
  206. package/lib/rename_tag_across_db.ts +19 -0
  207. package/lib/resolve_active_sheet_name.ts +36 -0
  208. package/lib/resolve_session_preferred_sheet.ts +37 -0
  209. package/lib/resolve_theme.ts +18 -0
  210. package/lib/resolve_theme_mode_to_theme.ts +19 -0
  211. package/lib/restore_db_from_uploaded_json.ts +24 -0
  212. package/lib/serialize_entry.ts +27 -0
  213. package/lib/serialize_reporting_source_sheets.ts +19 -0
  214. package/lib/serialize_sheet_entries.ts +18 -0
  215. package/lib/set_accent_color.ts +12 -0
  216. package/lib/set_active_sheet.ts +18 -0
  217. package/lib/set_color_palette.ts +12 -0
  218. package/lib/set_compact_lists.ts +12 -0
  219. package/lib/set_default_sheet_fixed_name.ts +8 -0
  220. package/lib/set_default_sheet_session_mode.ts +11 -0
  221. package/lib/set_sheet_tag_filter.ts +13 -0
  222. package/lib/set_theme_mode.ts +19 -0
  223. package/lib/sheet_tag_filter_snapshots.ts +48 -0
  224. package/lib/sort_serialized_entries.ts +35 -0
  225. package/lib/sort_sheet_report_stats.ts +43 -0
  226. package/lib/subscribe_compact_lists.ts +25 -0
  227. package/lib/subscribe_sheet_tag_filters.ts +28 -0
  228. package/lib/subscribe_theme.ts +23 -0
  229. package/lib/sync_active_sheet_preference.ts +19 -0
  230. package/lib/tags_are_equal.ts +12 -0
  231. package/lib/theme_init_script.ts +11 -0
  232. package/lib/toggle_sheet_tag_filter.ts +28 -0
  233. package/lib/toggle_theme.ts +20 -0
  234. package/lib/types/confirm_dialog.ts +9 -0
  235. package/lib/types/data.ts +16 -0
  236. package/lib/types/generic_data.ts +25 -0
  237. package/lib/types/index.ts +2 -0
  238. package/lib/types/reporting.ts +59 -0
  239. package/lib/types/tag_management.ts +7 -0
  240. package/lib/types/theme.ts +3 -0
  241. package/lib/types/tracker_state.ts +39 -0
  242. package/lib/types/ui_preferences.ts +104 -0
  243. package/lib/types/ui_settings.ts +17 -0
  244. package/lib/ui_preference_store.ts +80 -0
  245. package/lib/ui_settings_init_script.ts +33 -0
  246. package/lib/use_check_in_form_collapsed.ts +18 -0
  247. package/lib/use_clear_tag_filters_on_sheet_change.ts +18 -0
  248. package/lib/use_confirm_before_checkout.ts +18 -0
  249. package/lib/use_confirm_destructive_actions.ts +18 -0
  250. package/lib/use_duration_format.ts +17 -0
  251. package/lib/use_entry_list_sort.ts +17 -0
  252. package/lib/use_tag_filter_mode.ts +17 -0
  253. package/lib/use_time_format.ts +17 -0
  254. package/lib/use_timer_in_title.ts +18 -0
  255. package/lib/use_timer_show_seconds.ts +18 -0
  256. package/lib/use_week_starts_on.ts +17 -0
  257. package/lib/validate_entry_times.ts +12 -0
  258. package/lib/week_starts_on_to_index.ts +8 -0
  259. package/lib/write_active_sheet_preference.ts +28 -0
  260. package/lib/write_db.ts +20 -0
  261. package/lib/write_sheet_tag_filter.ts +20 -0
  262. package/lib/write_stored_compact_lists.ts +15 -0
  263. package/lib/write_stored_default_sheet_fixed_name.ts +28 -0
  264. package/lib/write_stored_default_sheet_session_mode.ts +24 -0
  265. package/lib/write_stored_sheet_tag_filters.ts +18 -0
  266. package/lib/write_stored_theme.ts +12 -0
  267. package/next.config.ts +7 -0
  268. package/package.json +96 -0
  269. package/pnpm-workspace.yaml +7 -0
  270. package/postcss.config.mjs +7 -0
  271. package/public/file.svg +1 -0
  272. package/public/globe.svg +1 -0
  273. package/public/next.svg +1 -0
  274. package/public/vercel.svg +1 -0
  275. package/public/window.svg +1 -0
  276. package/tsconfig.json +34 -0
@@ -0,0 +1,338 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react'
4
+
5
+ import { CheckInFormCollapsible } from '@/components/check-in-form-collapsible'
6
+ import { EntryTagFilter } from '@/components/entry-tag-filter'
7
+ import { EntryList } from '@/components/entry-list'
8
+ import { SheetSidebar } from '@/components/sheet-sidebar'
9
+ import { TrackerActiveBar } from '@/components/tracker-active-bar'
10
+ import { TrackerDocumentTitle } from '@/components/tracker-document-title'
11
+ import { TrackerTopbar } from '@/components/tracker-topbar'
12
+ import { build_resume_description } from '@/lib/build_resume_description'
13
+ import { collect_tags_from_entries } from '@/lib/collect_tags_from_entries'
14
+ import { filter_entries_by_tags } from '@/lib/filter_entries_by_tags'
15
+ import {
16
+ get_sheet_tag_filter_server_snapshot,
17
+ get_sheet_tag_filter_snapshot,
18
+ } from '@/lib/get_sheet_tag_filter_snapshot'
19
+ import { get_serialized_entries_total_ms } from '@/lib/get_serialized_entries_total_ms'
20
+ import { delete_tracker_action } from '@/lib/delete_tracker_action'
21
+ import { patch_tracker_action } from '@/lib/patch_tracker_action'
22
+ import { post_tracker_action } from '@/lib/post_tracker_action'
23
+ import { set_sheet_tag_filter } from '@/lib/set_sheet_tag_filter'
24
+ import { sort_serialized_entries } from '@/lib/sort_serialized_entries'
25
+ import { subscribe_sheet_tag_filters } from '@/lib/subscribe_sheet_tag_filters'
26
+ import { sync_active_sheet_preference } from '@/lib/sync_active_sheet_preference'
27
+ import { use_clear_tag_filters_on_sheet_change } from '@/lib/use_clear_tag_filters_on_sheet_change'
28
+ import { use_entry_list_sort } from '@/lib/use_entry_list_sort'
29
+ import { use_tag_filter_mode } from '@/lib/use_tag_filter_mode'
30
+ import { type EntryEditFormValues } from '@/components/entry-edit-form'
31
+ import { type TrackerState } from '@/lib/types/tracker_state'
32
+
33
+ interface TrackerAppProps {
34
+ initial_state: TrackerState
35
+ }
36
+
37
+ /**
38
+ * Main client application for the super-time-tracker web UI.
39
+ */
40
+ export function TrackerApp({ initial_state }: TrackerAppProps) {
41
+ const [state, set_state] = useState<TrackerState>(initial_state)
42
+ const [error, set_error] = useState<string | null>(null)
43
+ const [is_pending, set_is_pending] = useState(false)
44
+ useEffect(() => {
45
+ sync_active_sheet_preference(initial_state)
46
+ }, [initial_state])
47
+
48
+ const run_action = async (
49
+ action: () => Promise<TrackerState>,
50
+ ): Promise<void> => {
51
+ set_is_pending(true)
52
+ set_error(null)
53
+
54
+ try {
55
+ const next_state = await action()
56
+ sync_active_sheet_preference(next_state)
57
+ set_state(next_state)
58
+ } catch (action_error: unknown) {
59
+ set_error(
60
+ action_error instanceof Error
61
+ ? action_error.message
62
+ : String(action_error),
63
+ )
64
+ } finally {
65
+ set_is_pending(false)
66
+ }
67
+ }
68
+
69
+ const active_sheet =
70
+ state.sheets.find((sheet) => sheet.isActive)?.name ?? 'main'
71
+
72
+ const tag_filter_mode = use_tag_filter_mode()
73
+ const entry_list_sort = use_entry_list_sort()
74
+ const clear_tag_filters_on_sheet_change = use_clear_tag_filters_on_sheet_change()
75
+ const previous_active_sheet_ref = useRef<string | null>(null)
76
+
77
+ useEffect(() => {
78
+ if (
79
+ !clear_tag_filters_on_sheet_change ||
80
+ previous_active_sheet_ref.current === null ||
81
+ previous_active_sheet_ref.current === active_sheet
82
+ ) {
83
+ previous_active_sheet_ref.current = active_sheet
84
+ return
85
+ }
86
+
87
+ set_sheet_tag_filter(active_sheet, [])
88
+ previous_active_sheet_ref.current = active_sheet
89
+ }, [active_sheet, clear_tag_filters_on_sheet_change])
90
+
91
+ const filter_tags = useSyncExternalStore(
92
+ subscribe_sheet_tag_filters,
93
+ () => get_sheet_tag_filter_snapshot(active_sheet),
94
+ get_sheet_tag_filter_server_snapshot,
95
+ )
96
+
97
+ const sheet_tags = useMemo(
98
+ () => collect_tags_from_entries(state.activeSheetEntries),
99
+ [state.activeSheetEntries],
100
+ )
101
+
102
+ const filtered_entries = useMemo(() => {
103
+ const matching = filter_entries_by_tags(
104
+ state.activeSheetEntries,
105
+ filter_tags,
106
+ tag_filter_mode,
107
+ )
108
+
109
+ return sort_serialized_entries(matching, entry_list_sort)
110
+ }, [state.activeSheetEntries, filter_tags, tag_filter_mode, entry_list_sort])
111
+
112
+ const filtered_total_ms = useMemo(
113
+ () => get_serialized_entries_total_ms(filtered_entries),
114
+ [filtered_entries],
115
+ )
116
+
117
+ const entries_empty_message =
118
+ filter_tags.length > 0
119
+ ? tag_filter_mode === 'any'
120
+ ? `No entries on sheet "${active_sheet}" match any selected tag.`
121
+ : `No entries on sheet "${active_sheet}" match all selected tags.`
122
+ : `No entries on sheet "${active_sheet}".`
123
+
124
+ const edit_entry = (
125
+ sheet_name: string,
126
+ entry_id: number,
127
+ values: EntryEditFormValues,
128
+ ): Promise<TrackerState> =>
129
+ patch_tracker_action('/api/entry', {
130
+ sheetName: sheet_name,
131
+ entryId: entry_id,
132
+ ...values,
133
+ })
134
+
135
+ return (
136
+ <>
137
+ <TrackerDocumentTitle active_entry={state.activeEntry} />
138
+ <div className="relative z-1">
139
+ <TrackerTopbar />
140
+ <TrackerActiveBar
141
+ active_entry={state.activeEntry}
142
+ sheets={state.sheets}
143
+ is_pending={is_pending}
144
+ on_check_out={(at) =>
145
+ run_action(() =>
146
+ post_tracker_action('/api/out', {
147
+ sheetName: active_sheet,
148
+ ...(at !== undefined ? { at } : {}),
149
+ }),
150
+ )
151
+ }
152
+ on_delete={() =>
153
+ run_action(() =>
154
+ post_tracker_action('/api/entry', {
155
+ sheetName: active_sheet,
156
+ entryId: state.activeEntry?.id,
157
+ }),
158
+ )
159
+ }
160
+ on_edit={(values) =>
161
+ run_action(() =>
162
+ edit_entry(active_sheet, state.activeEntry?.id ?? 0, values),
163
+ )
164
+ }
165
+ on_move={(target_sheet_name) =>
166
+ run_action(() =>
167
+ post_tracker_action('/api/entry/move', {
168
+ sheetName: active_sheet,
169
+ entryId: state.activeEntry?.id,
170
+ targetSheetName: target_sheet_name,
171
+ }),
172
+ )
173
+ }
174
+ on_add_note={(text, at) =>
175
+ run_action(() =>
176
+ post_tracker_action('/api/note', {
177
+ text,
178
+ ...(at !== undefined ? { at } : {}),
179
+ sheetName: active_sheet,
180
+ entryId: state.activeEntry?.id,
181
+ }),
182
+ )
183
+ }
184
+ on_edit_note={(timestamp, text) =>
185
+ run_action(() =>
186
+ patch_tracker_action('/api/note', {
187
+ sheetName: active_sheet,
188
+ entryId: state.activeEntry?.id,
189
+ timestamp,
190
+ text,
191
+ }),
192
+ )
193
+ }
194
+ on_delete_note={(timestamp) =>
195
+ run_action(() =>
196
+ delete_tracker_action('/api/note', {
197
+ sheetName: active_sheet,
198
+ entryId: state.activeEntry?.id,
199
+ timestamp,
200
+ }),
201
+ )
202
+ }
203
+ />
204
+ </div>
205
+ <div className="mx-auto max-w-[1120px] px-5 pb-10 pt-5">
206
+ {error !== null ? (
207
+ <p className="mb-4 rounded-[0.65rem] border border-danger-border bg-danger-soft px-3 py-2.5 text-danger-text">
208
+ {error}
209
+ </p>
210
+ ) : null}
211
+
212
+ <div className="grid grid-cols-[minmax(16rem,20rem)_minmax(0,1fr)] items-start gap-4 max-[860px]:grid-cols-1">
213
+ <SheetSidebar
214
+ sheets={state.sheets}
215
+ db_path={state.dbPath}
216
+ is_pending={is_pending}
217
+ on_select={(name) =>
218
+ run_action(() => post_tracker_action('/api/sheet', { name }))
219
+ }
220
+ on_create={(name) =>
221
+ run_action(() => post_tracker_action('/api/sheet', { name }))
222
+ }
223
+ on_rename={(name, new_name) =>
224
+ run_action(() =>
225
+ patch_tracker_action('/api/sheet', { name, newName: new_name }),
226
+ )
227
+ }
228
+ on_delete={(name) =>
229
+ run_action(() =>
230
+ post_tracker_action('/api/sheet', { name, delete: true }),
231
+ )
232
+ }
233
+ />
234
+
235
+ <main className="flex min-w-0 flex-col gap-4 rounded-lg border border-panel-border bg-panel p-4 shadow-sm">
236
+ {state.activeEntry === null ? (
237
+ <CheckInFormCollapsible
238
+ known_tags={state.knownTags}
239
+ is_pending={is_pending}
240
+ on_submit={(values) =>
241
+ run_action(() =>
242
+ post_tracker_action('/api/in', {
243
+ ...values,
244
+ sheetName: active_sheet,
245
+ }),
246
+ )
247
+ }
248
+ />
249
+ ) : null}
250
+
251
+ <EntryTagFilter sheet_name={active_sheet} sheet_tags={sheet_tags} />
252
+
253
+ <EntryList
254
+ title="Entries"
255
+ entries={filtered_entries}
256
+ sheets={state.sheets}
257
+ total_ms={filtered_total_ms}
258
+ empty_message={entries_empty_message}
259
+ is_pending={is_pending}
260
+ show_sheet_name={false}
261
+ on_delete={(entry) =>
262
+ run_action(() =>
263
+ post_tracker_action('/api/entry', {
264
+ sheetName: entry.sheetName,
265
+ entryId: entry.id,
266
+ }),
267
+ )
268
+ }
269
+ on_edit={(entry, values) =>
270
+ run_action(() => edit_entry(entry.sheetName, entry.id, values))
271
+ }
272
+ on_move={(entry, target_sheet_name) =>
273
+ run_action(() =>
274
+ post_tracker_action('/api/entry/move', {
275
+ sheetName: entry.sheetName,
276
+ entryId: entry.id,
277
+ targetSheetName: target_sheet_name,
278
+ }),
279
+ )
280
+ }
281
+ on_move_many={(entries, target_sheet_name) =>
282
+ run_action(() =>
283
+ post_tracker_action('/api/entry/move-bulk', {
284
+ entries: entries.map((entry) => ({
285
+ sheetName: entry.sheetName,
286
+ entryId: entry.id,
287
+ })),
288
+ targetSheetName: target_sheet_name,
289
+ }),
290
+ )
291
+ }
292
+ on_delete_many={(entries) =>
293
+ run_action(() =>
294
+ post_tracker_action('/api/entry/delete-bulk', {
295
+ entries: entries.map((entry) => ({
296
+ sheetName: entry.sheetName,
297
+ entryId: entry.id,
298
+ })),
299
+ }),
300
+ )
301
+ }
302
+ on_edit_note={(entry, timestamp, text) =>
303
+ run_action(() =>
304
+ patch_tracker_action('/api/note', {
305
+ sheetName: entry.sheetName,
306
+ entryId: entry.id,
307
+ timestamp,
308
+ text,
309
+ }),
310
+ )
311
+ }
312
+ on_add_note={(entry, text) =>
313
+ run_action(() =>
314
+ post_tracker_action('/api/note', {
315
+ sheetName: entry.sheetName,
316
+ entryId: entry.id,
317
+ text,
318
+ }),
319
+ )
320
+ }
321
+ on_resume={(entry) =>
322
+ run_action(() =>
323
+ post_tracker_action('/api/in', {
324
+ description: build_resume_description(
325
+ entry.description,
326
+ entry.tags,
327
+ ),
328
+ sheetName: entry.sheetName,
329
+ }),
330
+ )
331
+ }
332
+ />
333
+ </main>
334
+ </div>
335
+ </div>
336
+ </>
337
+ )
338
+ }
@@ -0,0 +1,56 @@
1
+ import Link from 'next/link'
2
+
3
+ export interface TrackerBreadcrumbSegment {
4
+ label: string
5
+ href?: string
6
+ }
7
+
8
+ interface TrackerBreadcrumbProps {
9
+ current: string
10
+ parent?: TrackerBreadcrumbSegment
11
+ }
12
+
13
+ /**
14
+ * Breadcrumb trail from the tracker home to a sub-page.
15
+ */
16
+ export function TrackerBreadcrumb({ current, parent }: TrackerBreadcrumbProps) {
17
+ return (
18
+ <nav aria-label="Breadcrumb" className="min-w-0 text-left">
19
+ <ol className="m-0 flex list-none flex-wrap items-center gap-1.5 p-0 text-[0.85rem]">
20
+ <li>
21
+ <Link
22
+ className="text-muted no-underline hover:text-foreground"
23
+ href="/"
24
+ >
25
+ Tracker
26
+ </Link>
27
+ </li>
28
+ {parent !== undefined ? (
29
+ <>
30
+ <li className="text-muted" aria-hidden="true">
31
+ /
32
+ </li>
33
+ <li>
34
+ {parent.href !== undefined ? (
35
+ <Link
36
+ className="text-muted no-underline hover:text-foreground"
37
+ href={parent.href}
38
+ >
39
+ {parent.label}
40
+ </Link>
41
+ ) : (
42
+ <span className="text-muted">{parent.label}</span>
43
+ )}
44
+ </li>
45
+ </>
46
+ ) : null}
47
+ <li className="text-muted" aria-hidden="true">
48
+ /
49
+ </li>
50
+ <li className="font-medium text-foreground" aria-current="page">
51
+ {current}
52
+ </li>
53
+ </ol>
54
+ </nav>
55
+ )
56
+ }
@@ -0,0 +1,67 @@
1
+ 'use client'
2
+
3
+ import { useEffect, useState } from 'react'
4
+
5
+ import { format_duration } from '@/lib/format_duration'
6
+ import { use_duration_format } from '@/lib/use_duration_format'
7
+ import { use_timer_in_title } from '@/lib/use_timer_in_title'
8
+ import { use_timer_show_seconds } from '@/lib/use_timer_show_seconds'
9
+ import { type SerializedEntry } from '@/lib/types/tracker_state'
10
+
11
+ interface TrackerDocumentTitleProps {
12
+ active_entry: SerializedEntry | null
13
+ }
14
+
15
+ const base_title = 'super-time-tracker'
16
+
17
+ /**
18
+ * Updates the document title with the live timer when enabled.
19
+ */
20
+ export function TrackerDocumentTitle({ active_entry }: TrackerDocumentTitleProps) {
21
+ const timer_in_title = use_timer_in_title()
22
+ const duration_format = use_duration_format()
23
+ const show_seconds = use_timer_show_seconds()
24
+ const [duration_ms, set_duration_ms] = useState(
25
+ active_entry?.durationMs ?? 0,
26
+ )
27
+
28
+ useEffect(() => {
29
+ if (active_entry === null) {
30
+ set_duration_ms(0)
31
+ return
32
+ }
33
+
34
+ set_duration_ms(active_entry.durationMs)
35
+
36
+ const interval = window.setInterval(() => {
37
+ set_duration_ms(Date.now() - new Date(active_entry.start).getTime())
38
+ }, 1000)
39
+
40
+ return () => window.clearInterval(interval)
41
+ }, [active_entry])
42
+
43
+ useEffect(() => {
44
+ if (!timer_in_title || active_entry === null) {
45
+ document.title = base_title
46
+ return
47
+ }
48
+
49
+ const label =
50
+ active_entry.description.trim() || 'Tracking'
51
+ const duration = format_duration(duration_ms, duration_format, show_seconds)
52
+
53
+ document.title = `${duration} — ${label}`
54
+
55
+ return () => {
56
+ document.title = base_title
57
+ }
58
+ }, [
59
+ active_entry,
60
+ duration_format,
61
+ duration_ms,
62
+ show_seconds,
63
+ timer_in_title,
64
+ ])
65
+
66
+ return null
67
+ }
@@ -0,0 +1,63 @@
1
+ 'use client'
2
+
3
+ import Link from 'next/link'
4
+
5
+ import { ThemeSwitcher } from '@/components/theme_switcher'
6
+ import {
7
+ TrackerBreadcrumb,
8
+ type TrackerBreadcrumbSegment,
9
+ } from '@/components/tracker-breadcrumb'
10
+
11
+ export interface TrackerTopbarBreadcrumb {
12
+ current: string
13
+ parent?: TrackerBreadcrumbSegment
14
+ }
15
+
16
+ interface TrackerTopbarProps {
17
+ breadcrumb?: TrackerTopbarBreadcrumb
18
+ }
19
+
20
+ /**
21
+ * Sticky app navbar with branding, breadcrumbs, and theme switcher.
22
+ */
23
+ export function TrackerTopbar({ breadcrumb }: TrackerTopbarProps = {}) {
24
+ return (
25
+ <header className="relative z-1 border-b border-panel-border bg-[color-mix(in_srgb,var(--panel)_92%,var(--background))] shadow-sm backdrop-blur-[10px]">
26
+ <div className="mx-auto flex max-w-[1120px] min-h-13 flex-wrap items-center gap-x-4 gap-y-2 px-5">
27
+ <span className="inline-flex shrink-0 items-center font-mono text-[0.72rem] font-semibold uppercase leading-tight tracking-[0.08em] text-accent whitespace-nowrap">
28
+ super-time-tracker
29
+ </span>
30
+ {breadcrumb !== undefined ? (
31
+ <TrackerBreadcrumb current={breadcrumb.current} parent={breadcrumb.parent} />
32
+ ) : null}
33
+ <div className="ml-auto flex shrink-0 items-center justify-end gap-2">
34
+ <Link
35
+ className="rounded-full px-3 py-1.5 text-[0.85rem] font-semibold text-muted no-underline hover:bg-surface-hover hover:text-foreground"
36
+ href="/"
37
+ >
38
+ Tracker
39
+ </Link>
40
+ <Link
41
+ className="rounded-full px-3 py-1.5 text-[0.85rem] font-semibold text-muted no-underline hover:bg-surface-hover hover:text-foreground"
42
+ href="/reporting"
43
+ >
44
+ Reporting
45
+ </Link>
46
+ <Link
47
+ className="rounded-full px-3 py-1.5 text-[0.85rem] font-semibold text-muted no-underline hover:bg-surface-hover hover:text-foreground"
48
+ href="/settings/tags"
49
+ >
50
+ Manage tags
51
+ </Link>
52
+ <Link
53
+ className="rounded-full px-3 py-1.5 text-[0.85rem] font-semibold text-muted no-underline hover:bg-surface-hover hover:text-foreground"
54
+ href="/settings"
55
+ >
56
+ Settings
57
+ </Link>
58
+ <ThemeSwitcher />
59
+ </div>
60
+ </div>
61
+ </header>
62
+ )
63
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Renders a small trash icon for delete actions.
3
+ */
4
+ export function TrashIcon() {
5
+ return (
6
+ <svg
7
+ className="h-[0.85rem] w-[0.85rem]"
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ viewBox="0 0 24 24"
10
+ fill="none"
11
+ stroke="currentColor"
12
+ strokeWidth="2"
13
+ strokeLinecap="round"
14
+ strokeLinejoin="round"
15
+ aria-hidden="true"
16
+ >
17
+ <path d="M3 6h18" />
18
+ <path d="M8 6V4h8v2" />
19
+ <path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
20
+ <path d="M10 11v6" />
21
+ <path d="M14 11v6" />
22
+ </svg>
23
+ )
24
+ }
@@ -0,0 +1,39 @@
1
+ 'use client'
2
+
3
+ import { useSyncExternalStore } from 'react'
4
+
5
+ import { SettingRadioGroup } from '@/components/setting-radio-group'
6
+ import { week_starts_on_preference } from '@/lib/preferences/week_starts_on_preference'
7
+ import { persist_ui_preference } from '@/lib/persist_ui_preference'
8
+ import { type WeekStartsOn } from '@/lib/types/ui_preferences'
9
+
10
+ const options: { value: WeekStartsOn; label: string; description: string }[] = [
11
+ { value: 'monday', label: 'Monday', description: 'ISO 8601 week (default).' },
12
+ { value: 'sunday', label: 'Sunday', description: 'US-style week.' },
13
+ ]
14
+
15
+ const set_week_starts_on = (value: WeekStartsOn): void => {
16
+ persist_ui_preference(week_starts_on_preference, value)
17
+ }
18
+
19
+ /**
20
+ * Setting: which day starts a week in reporting shortcuts.
21
+ */
22
+ export function WeekStartsOnSetting() {
23
+ const value = useSyncExternalStore(
24
+ week_starts_on_preference.subscribe,
25
+ week_starts_on_preference.get_snapshot,
26
+ week_starts_on_preference.get_server_snapshot,
27
+ )
28
+
29
+ return (
30
+ <SettingRadioGroup<WeekStartsOn>
31
+ name="week-starts-on"
32
+ legend="Week starts on"
33
+ description={'Used for the "this week" reporting shortcuts and totals.'}
34
+ value={value}
35
+ options={options}
36
+ on_change={set_week_starts_on}
37
+ />
38
+ )
39
+ }
@@ -0,0 +1,18 @@
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
@@ -0,0 +1,65 @@
1
+ import { find_sheet_with_active_entry } from "@/lib/find_sheet_with_active_entry";
2
+ import { get_sheet } from "@/lib/get_sheet";
3
+ import { parse_natural_language_date } from "@/lib/parse_natural_language_date";
4
+ import { read_db } from "@/lib/read_db";
5
+ import { write_db } from "@/lib/write_db";
6
+
7
+ export interface AddNoteToEntryArgs {
8
+ text: string;
9
+ sheet_name?: string;
10
+ entry_id?: number;
11
+ at?: string;
12
+ }
13
+
14
+ /**
15
+ * Appends a timestamped note to the active or specified entry.
16
+ */
17
+ export async function add_note_to_entry(
18
+ args: AddNoteToEntryArgs,
19
+ ): Promise<void> {
20
+ const {
21
+ text,
22
+ at,
23
+ entry_id: input_entry_id,
24
+ sheet_name: input_sheet_name,
25
+ } = args;
26
+ const db = await read_db();
27
+ const sheet =
28
+ input_sheet_name !== undefined && input_sheet_name.length > 0
29
+ ? get_sheet(db, input_sheet_name)
30
+ : find_sheet_with_active_entry(db);
31
+
32
+ if (sheet === null) {
33
+ throw new Error("No active sheet");
34
+ }
35
+
36
+ const { name: sheet_name } = sheet;
37
+ const entry_id = input_entry_id ?? sheet.activeEntryID;
38
+
39
+ if (entry_id === null) {
40
+ throw new Error(`Sheet ${sheet_name} has no active entry`);
41
+ }
42
+
43
+ const entry = sheet.entries.find(({ id }) => id === entry_id);
44
+
45
+ if (entry === undefined) {
46
+ throw new Error(`Entry ${entry_id} not found in sheet ${sheet_name}`);
47
+ }
48
+
49
+ const note_timestamp =
50
+ at === undefined || at.trim().length === 0
51
+ ? new Date()
52
+ : parse_natural_language_date(at);
53
+ const latest_allowed = entry.end ?? new Date();
54
+
55
+ if (+note_timestamp < +entry.start) {
56
+ throw new Error("Note time must be on or after entry start");
57
+ }
58
+
59
+ if (+note_timestamp > +latest_allowed) {
60
+ throw new Error("Note time must be on or before entry end");
61
+ }
62
+
63
+ entry.notes.push({ timestamp: note_timestamp, text });
64
+ await write_db(db);
65
+ }
@@ -0,0 +1,10 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ /**
4
+ * Builds a JSON error response from an unknown thrown value.
5
+ */
6
+ export function api_error_response(error: unknown, status = 400): NextResponse {
7
+ const message = error instanceof Error ? error.message : String(error);
8
+
9
+ return NextResponse.json({ error: message }, { status });
10
+ }