@burdenoff/microfe-timecampus 2026.828.1 → 2026.830.1
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/dist/timecampus/hooks/index.d.ts +1 -0
- package/dist/timecampus/hooks/index.d.ts.map +1 -1
- package/dist/timecampus/hooks/index.js +3 -0
- package/dist/timecampus/hooks/useToast.js +1 -0
- package/dist/timecampus/pages/HomePage.d.ts.map +1 -1
- package/dist/timecampus/pages/HomePage.js +129 -131
- package/dist/timecampus/pages/HomePage.js.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.d.ts.map +1 -1
- package/dist/timecampus/pages/time/TimerPage.js +62 -60
- package/dist/timecampus/pages/time/TimerPage.js.map +1 -1
- package/dist/timecampus/shared/hooks/useTr.d.ts +17 -0
- package/dist/timecampus/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/timecampus/shared/hooks/useTr.js +14 -0
- package/dist/timecampus/shared/hooks/useTr.js.map +1 -0
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/timecampus/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/timecampus/hooks/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAGH,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAGtC,OAAO,EAAE,KAAK,EAAE,MAAM,uBAAuB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "react";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomePage.d.ts","sourceRoot":"","sources":["../../../src/timecampus/pages/HomePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA6BhC,eAAO,MAAM,QAAQ,EAAE,
|
|
1
|
+
{"version":3,"file":"HomePage.d.ts","sourceRoot":"","sources":["../../../src/timecampus/pages/HomePage.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC;AA6BhC,eAAO,MAAM,QAAQ,EAAE,EAwWtB,CAAC"}
|
|
@@ -3,254 +3,252 @@ import { cn as t, formatHours as n } from "../utils/helpers.js";
|
|
|
3
3
|
import { StatCard as r } from "../components/StatCard.js";
|
|
4
4
|
import { LoadingSkeleton as i } from "../components/LoadingSkeleton.js";
|
|
5
5
|
import { useGetTimeEntryStatsQuery as a, useListTimeEntriesQuery as o } from "../../generated/wspace-operations.js";
|
|
6
|
-
import {
|
|
7
|
-
import
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
6
|
+
import { useTr as s } from "../shared/hooks/useTr.js";
|
|
7
|
+
import "../hooks/index.js";
|
|
8
|
+
import { ArrowRight as c, BarChart3 as l, CheckCircle as u, Clock as d, FolderOpen as f, Play as p, TrendingUp as m, Users as h } from "lucide-react";
|
|
9
|
+
import { jsx as g, jsxs as _ } from "react/jsx-runtime";
|
|
10
|
+
import { AuroraBackground as v, CTAOverflowMenu as y, EmphasisPanel as b, GlassCard as x, GradientText as S } from "@burdenoff/fe-libs/ui";
|
|
10
11
|
//#region src/timecampus/pages/HomePage.tsx
|
|
11
12
|
var C = () => {
|
|
12
|
-
let {
|
|
13
|
-
let n = C(e);
|
|
14
|
-
return n === e ? t : n;
|
|
15
|
-
}, T = e(), { data: E, loading: D } = a(), { data: O, loading: k } = o({ variables: { pagination: { limit: 4 } } }), A = E?.timeEntryStats, j = O?.timeEntries?.items ?? [], M = /* @__PURE__ */ h(v, {
|
|
13
|
+
let C = s(), w = e(), { data: T, loading: E } = a(), { data: D, loading: O } = o({ variables: { pagination: { limit: 4 } } }), k = T?.timeEntryStats, A = D?.timeEntries?.items ?? [], j = /* @__PURE__ */ g(y, {
|
|
16
14
|
primary: {
|
|
17
|
-
label:
|
|
18
|
-
onSelect: () =>
|
|
19
|
-
icon: /* @__PURE__ */
|
|
15
|
+
label: C("timecampus.home.actions.startTimer", "Start Timer"),
|
|
16
|
+
onSelect: () => w("/time/timer"),
|
|
17
|
+
icon: /* @__PURE__ */ g(p, { className: "h-4 w-4" })
|
|
20
18
|
},
|
|
21
19
|
actions: [{
|
|
22
|
-
label:
|
|
23
|
-
onSelect: () =>
|
|
24
|
-
icon: /* @__PURE__ */
|
|
20
|
+
label: C("timecampus.home.quickActions.viewProjects.label", "View Projects"),
|
|
21
|
+
onSelect: () => w("/projects"),
|
|
22
|
+
icon: /* @__PURE__ */ g(f, { className: "h-4 w-4" })
|
|
25
23
|
}, {
|
|
26
|
-
label:
|
|
27
|
-
onSelect: () =>
|
|
28
|
-
icon: /* @__PURE__ */
|
|
24
|
+
label: C("timecampus.home.quickActions.reports.label", "Reports"),
|
|
25
|
+
onSelect: () => w("/reports"),
|
|
26
|
+
icon: /* @__PURE__ */ g(l, { className: "h-4 w-4" })
|
|
29
27
|
}]
|
|
30
|
-
}),
|
|
28
|
+
}), M = () => /* @__PURE__ */ _("div", {
|
|
31
29
|
className: "relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6",
|
|
32
|
-
children: [/* @__PURE__ */
|
|
30
|
+
children: [/* @__PURE__ */ g(v, { intensity: "subtle" }), /* @__PURE__ */ _("div", {
|
|
33
31
|
className: "relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between",
|
|
34
|
-
children: [/* @__PURE__ */
|
|
32
|
+
children: [/* @__PURE__ */ _("div", {
|
|
35
33
|
className: "min-w-0",
|
|
36
|
-
children: [/* @__PURE__ */
|
|
34
|
+
children: [/* @__PURE__ */ g("h1", {
|
|
37
35
|
className: "text-2xl font-bold text-text-primary",
|
|
38
|
-
children: /* @__PURE__ */
|
|
39
|
-
}), /* @__PURE__ */
|
|
36
|
+
children: /* @__PURE__ */ g(S, { children: C("timecampus.home.title", "Dashboard") })
|
|
37
|
+
}), /* @__PURE__ */ g("p", {
|
|
40
38
|
className: "mt-1 text-sm text-text-secondary",
|
|
41
|
-
children:
|
|
39
|
+
children: C("timecampus.home.description", "Overview of your time tracking and productivity")
|
|
42
40
|
})]
|
|
43
|
-
}),
|
|
41
|
+
}), j]
|
|
44
42
|
})]
|
|
45
43
|
});
|
|
46
|
-
return
|
|
44
|
+
return E && O ? /* @__PURE__ */ _("div", {
|
|
47
45
|
className: "min-h-full space-y-6 p-4 sm:p-6",
|
|
48
|
-
children: [
|
|
46
|
+
children: [M(), /* @__PURE__ */ g(i, {
|
|
49
47
|
variant: "card",
|
|
50
48
|
count: 4
|
|
51
49
|
})]
|
|
52
|
-
}) : /* @__PURE__ */
|
|
50
|
+
}) : /* @__PURE__ */ _("div", {
|
|
53
51
|
className: "min-h-full space-y-6 p-4 sm:p-6",
|
|
54
52
|
children: [
|
|
55
|
-
|
|
56
|
-
/* @__PURE__ */
|
|
53
|
+
M(),
|
|
54
|
+
/* @__PURE__ */ _("div", {
|
|
57
55
|
className: "grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4",
|
|
58
56
|
children: [
|
|
59
|
-
/* @__PURE__ */
|
|
57
|
+
/* @__PURE__ */ g(r, {
|
|
60
58
|
featured: !0,
|
|
61
|
-
title:
|
|
62
|
-
value: n(
|
|
63
|
-
icon: /* @__PURE__ */
|
|
59
|
+
title: C("timecampus.home.stats.today", "Today"),
|
|
60
|
+
value: n(k?.totalHours ?? 0),
|
|
61
|
+
icon: /* @__PURE__ */ g(d, { className: "h-5 w-5" }),
|
|
64
62
|
trend: {
|
|
65
63
|
value: 12.5,
|
|
66
|
-
label:
|
|
64
|
+
label: C("timecampus.home.stats.vsYesterday", "vs yesterday")
|
|
67
65
|
}
|
|
68
66
|
}),
|
|
69
|
-
/* @__PURE__ */
|
|
70
|
-
title:
|
|
71
|
-
value: n(
|
|
72
|
-
icon: /* @__PURE__ */
|
|
73
|
-
description: `${
|
|
67
|
+
/* @__PURE__ */ g(r, {
|
|
68
|
+
title: C("timecampus.home.stats.billable", "Billable"),
|
|
69
|
+
value: n(k?.billableHours ?? 0),
|
|
70
|
+
icon: /* @__PURE__ */ g(m, { className: "h-5 w-5" }),
|
|
71
|
+
description: `${k?.totalEntries ?? 0} ${C("timecampus.home.stats.entries", "entries")}`
|
|
74
72
|
}),
|
|
75
|
-
/* @__PURE__ */
|
|
76
|
-
title:
|
|
77
|
-
value:
|
|
78
|
-
icon: /* @__PURE__ */
|
|
79
|
-
description:
|
|
73
|
+
/* @__PURE__ */ g(r, {
|
|
74
|
+
title: C("timecampus.home.stats.activeTimers", "Active Timers"),
|
|
75
|
+
value: k?.activeTimers ?? 0,
|
|
76
|
+
icon: /* @__PURE__ */ g(f, { className: "h-5 w-5" }),
|
|
77
|
+
description: C("timecampus.home.stats.currentlyRunning", "Currently running")
|
|
80
78
|
}),
|
|
81
|
-
/* @__PURE__ */
|
|
82
|
-
title:
|
|
83
|
-
value:
|
|
84
|
-
icon: /* @__PURE__ */
|
|
85
|
-
description:
|
|
79
|
+
/* @__PURE__ */ g(r, {
|
|
80
|
+
title: C("timecampus.home.stats.totalEntries", "Entries"),
|
|
81
|
+
value: k?.totalEntries ?? 0,
|
|
82
|
+
icon: /* @__PURE__ */ g(l, { className: "h-5 w-5" }),
|
|
83
|
+
description: C("timecampus.home.stats.totalEntriesDescription", "Total time entries")
|
|
86
84
|
})
|
|
87
85
|
]
|
|
88
86
|
}),
|
|
89
|
-
/* @__PURE__ */
|
|
87
|
+
/* @__PURE__ */ _("div", {
|
|
90
88
|
className: "grid grid-cols-1 gap-6 lg:grid-cols-3",
|
|
91
|
-
children: [/* @__PURE__ */
|
|
89
|
+
children: [/* @__PURE__ */ _(x, {
|
|
92
90
|
treatment: "plain",
|
|
93
91
|
className: "border-border-seam p-6 lg:col-span-2",
|
|
94
|
-
children: [/* @__PURE__ */
|
|
92
|
+
children: [/* @__PURE__ */ _("div", {
|
|
95
93
|
className: "mb-4 flex items-center justify-between",
|
|
96
|
-
children: [/* @__PURE__ */
|
|
94
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("h3", {
|
|
97
95
|
className: "text-sm font-semibold text-text-primary",
|
|
98
|
-
children:
|
|
99
|
-
}), /* @__PURE__ */
|
|
96
|
+
children: C("timecampus.home.recentEntries.title", "Recent Time Entries")
|
|
97
|
+
}), /* @__PURE__ */ g("p", {
|
|
100
98
|
className: "text-xs text-text-secondary",
|
|
101
|
-
children:
|
|
102
|
-
})] }), /* @__PURE__ */
|
|
103
|
-
onClick: () =>
|
|
99
|
+
children: C("timecampus.home.recentEntries.description", "Your latest tracked activities")
|
|
100
|
+
})] }), /* @__PURE__ */ _("button", {
|
|
101
|
+
onClick: () => w("/time/entries"),
|
|
104
102
|
className: "inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border",
|
|
105
103
|
children: [
|
|
106
|
-
|
|
104
|
+
C("timecampus.home.recentEntries.viewAll", "View all"),
|
|
107
105
|
" ",
|
|
108
|
-
/* @__PURE__ */
|
|
106
|
+
/* @__PURE__ */ g(c, { className: "h-3 w-3" })
|
|
109
107
|
]
|
|
110
108
|
})]
|
|
111
|
-
}),
|
|
109
|
+
}), O ? /* @__PURE__ */ g(i, {
|
|
112
110
|
variant: "list",
|
|
113
111
|
count: 4
|
|
114
|
-
}) : /* @__PURE__ */
|
|
112
|
+
}) : /* @__PURE__ */ _("div", {
|
|
115
113
|
className: "flex flex-col gap-3",
|
|
116
|
-
children: [
|
|
114
|
+
children: [A.map((e) => /* @__PURE__ */ _("div", {
|
|
117
115
|
className: "flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50",
|
|
118
|
-
children: [/* @__PURE__ */
|
|
116
|
+
children: [/* @__PURE__ */ _("div", {
|
|
119
117
|
className: "min-w-0 flex-1",
|
|
120
|
-
children: [/* @__PURE__ */
|
|
118
|
+
children: [/* @__PURE__ */ g("p", {
|
|
121
119
|
className: "text-sm font-medium text-text-primary",
|
|
122
|
-
children: e.project?.name ??
|
|
123
|
-
}), /* @__PURE__ */
|
|
120
|
+
children: e.project?.name ?? C("timecampus.home.recentEntries.noProject", "No Project")
|
|
121
|
+
}), /* @__PURE__ */ g("p", {
|
|
124
122
|
className: "text-xs text-text-secondary",
|
|
125
|
-
children: e.description ??
|
|
123
|
+
children: e.description ?? C("timecampus.home.recentEntries.noDescription", "No description")
|
|
126
124
|
})]
|
|
127
|
-
}), /* @__PURE__ */
|
|
125
|
+
}), /* @__PURE__ */ _("div", {
|
|
128
126
|
className: "ml-4 text-right",
|
|
129
|
-
children: [/* @__PURE__ */
|
|
127
|
+
children: [/* @__PURE__ */ g("p", {
|
|
130
128
|
className: "font-mono text-sm font-medium tabular-nums text-text-primary",
|
|
131
129
|
children: n((e.duration ?? 0) / 3600)
|
|
132
|
-
}), /* @__PURE__ */
|
|
130
|
+
}), /* @__PURE__ */ _("span", {
|
|
133
131
|
className: t("inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium", e.isRunning ? "bg-status-info-bg text-status-info-text" : "bg-status-success-bg text-status-success-text"),
|
|
134
|
-
children: [e.isRunning && /* @__PURE__ */
|
|
132
|
+
children: [e.isRunning && /* @__PURE__ */ g("span", { className: "h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse" }), e.isRunning ? C("timecampus.home.recentEntries.status.running", "running") : C("timecampus.home.recentEntries.status.completed", "completed")]
|
|
135
133
|
})]
|
|
136
134
|
})]
|
|
137
|
-
}, e.id)),
|
|
135
|
+
}, e.id)), A.length === 0 && /* @__PURE__ */ g("p", {
|
|
138
136
|
className: "py-4 text-center text-sm text-text-secondary",
|
|
139
|
-
children:
|
|
137
|
+
children: C("timecampus.home.recentEntries.empty", "No time entries yet")
|
|
140
138
|
})]
|
|
141
139
|
})]
|
|
142
|
-
}), /* @__PURE__ */
|
|
140
|
+
}), /* @__PURE__ */ _(b, {
|
|
143
141
|
className: "p-6",
|
|
144
|
-
children: [/* @__PURE__ */
|
|
142
|
+
children: [/* @__PURE__ */ _("div", {
|
|
145
143
|
className: "mb-4",
|
|
146
|
-
children: [/* @__PURE__ */
|
|
144
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
147
145
|
className: "text-sm font-semibold text-text-primary",
|
|
148
|
-
children:
|
|
149
|
-
}), /* @__PURE__ */
|
|
146
|
+
children: C("timecampus.home.summary.title", "Quick Summary")
|
|
147
|
+
}), /* @__PURE__ */ g("p", {
|
|
150
148
|
className: "text-xs text-text-secondary",
|
|
151
|
-
children:
|
|
149
|
+
children: C("timecampus.home.summary.description", "Time tracking overview")
|
|
152
150
|
})]
|
|
153
|
-
}), /* @__PURE__ */
|
|
151
|
+
}), /* @__PURE__ */ _("div", {
|
|
154
152
|
className: "flex flex-col gap-4",
|
|
155
153
|
children: [
|
|
156
|
-
/* @__PURE__ */
|
|
154
|
+
/* @__PURE__ */ _("div", {
|
|
157
155
|
className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
|
|
158
|
-
children: [/* @__PURE__ */
|
|
156
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
159
157
|
className: "text-xs font-medium text-text-secondary",
|
|
160
|
-
children:
|
|
161
|
-
}), /* @__PURE__ */
|
|
158
|
+
children: C("timecampus.home.summary.totalHours", "Total Hours")
|
|
159
|
+
}), /* @__PURE__ */ g("p", {
|
|
162
160
|
className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
|
|
163
|
-
children: n(
|
|
164
|
-
})] }), /* @__PURE__ */
|
|
161
|
+
children: n(k?.totalHours ?? 0)
|
|
162
|
+
})] }), /* @__PURE__ */ g(d, { className: "h-5 w-5 text-text-secondary" })]
|
|
165
163
|
}),
|
|
166
|
-
/* @__PURE__ */
|
|
164
|
+
/* @__PURE__ */ _("div", {
|
|
167
165
|
className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
|
|
168
|
-
children: [/* @__PURE__ */
|
|
166
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
169
167
|
className: "text-xs font-medium text-text-secondary",
|
|
170
|
-
children:
|
|
171
|
-
}), /* @__PURE__ */
|
|
168
|
+
children: C("timecampus.home.summary.billableHours", "Billable Hours")
|
|
169
|
+
}), /* @__PURE__ */ g("p", {
|
|
172
170
|
className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
|
|
173
|
-
children: n(
|
|
174
|
-
})] }), /* @__PURE__ */
|
|
171
|
+
children: n(k?.billableHours ?? 0)
|
|
172
|
+
})] }), /* @__PURE__ */ g(m, { className: "h-5 w-5 text-text-secondary" })]
|
|
175
173
|
}),
|
|
176
|
-
/* @__PURE__ */
|
|
174
|
+
/* @__PURE__ */ _("div", {
|
|
177
175
|
className: "flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3",
|
|
178
|
-
children: [/* @__PURE__ */
|
|
176
|
+
children: [/* @__PURE__ */ _("div", { children: [/* @__PURE__ */ g("p", {
|
|
179
177
|
className: "text-xs font-medium text-text-secondary",
|
|
180
|
-
children:
|
|
181
|
-
}), /* @__PURE__ */
|
|
178
|
+
children: C("timecampus.home.summary.activeTimers", "Active Timers")
|
|
179
|
+
}), /* @__PURE__ */ g("p", {
|
|
182
180
|
className: "mt-1 text-xl font-bold tabular-nums text-text-primary",
|
|
183
|
-
children:
|
|
184
|
-
})] }), /* @__PURE__ */ h
|
|
181
|
+
children: k?.activeTimers ?? 0
|
|
182
|
+
})] }), /* @__PURE__ */ g(h, { className: "h-5 w-5 text-text-secondary" })]
|
|
185
183
|
})
|
|
186
184
|
]
|
|
187
185
|
})]
|
|
188
186
|
})]
|
|
189
187
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
188
|
+
/* @__PURE__ */ _(x, {
|
|
191
189
|
treatment: "plain",
|
|
192
190
|
className: "border-border-seam p-6",
|
|
193
|
-
children: [/* @__PURE__ */
|
|
191
|
+
children: [/* @__PURE__ */ _("div", {
|
|
194
192
|
className: "mb-4",
|
|
195
|
-
children: [/* @__PURE__ */
|
|
193
|
+
children: [/* @__PURE__ */ g("h3", {
|
|
196
194
|
className: "text-sm font-semibold text-text-primary",
|
|
197
|
-
children:
|
|
198
|
-
}), /* @__PURE__ */
|
|
195
|
+
children: C("timecampus.home.quickActions.title", "Quick Actions")
|
|
196
|
+
}), /* @__PURE__ */ g("p", {
|
|
199
197
|
className: "text-xs text-text-secondary",
|
|
200
|
-
children:
|
|
198
|
+
children: C("timecampus.home.quickActions.description", "Frequently used features")
|
|
201
199
|
})]
|
|
202
|
-
}), /* @__PURE__ */
|
|
200
|
+
}), /* @__PURE__ */ g("div", {
|
|
203
201
|
className: "grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6",
|
|
204
202
|
children: [
|
|
205
203
|
{
|
|
206
|
-
label:
|
|
207
|
-
icon:
|
|
204
|
+
label: C("timecampus.home.quickActions.startTimer.label", "Start Timer"),
|
|
205
|
+
icon: p,
|
|
208
206
|
route: "/time/timer",
|
|
209
|
-
description:
|
|
207
|
+
description: C("timecampus.home.quickActions.startTimer.description", "Track your time")
|
|
210
208
|
},
|
|
211
209
|
{
|
|
212
|
-
label:
|
|
213
|
-
icon:
|
|
210
|
+
label: C("timecampus.home.quickActions.viewProjects.label", "View Projects"),
|
|
211
|
+
icon: f,
|
|
214
212
|
route: "/projects",
|
|
215
|
-
description:
|
|
213
|
+
description: C("timecampus.home.quickActions.viewProjects.description", "Manage projects")
|
|
216
214
|
},
|
|
217
215
|
{
|
|
218
|
-
label:
|
|
219
|
-
icon:
|
|
216
|
+
label: C("timecampus.home.quickActions.timeEntries.label", "Time Entries"),
|
|
217
|
+
icon: d,
|
|
220
218
|
route: "/time/entries",
|
|
221
|
-
description:
|
|
219
|
+
description: C("timecampus.home.quickActions.timeEntries.description", "View all entries")
|
|
222
220
|
},
|
|
223
221
|
{
|
|
224
|
-
label:
|
|
225
|
-
icon:
|
|
222
|
+
label: C("timecampus.home.quickActions.teamActivity.label", "Team Activity"),
|
|
223
|
+
icon: h,
|
|
226
224
|
route: "/team-activity",
|
|
227
|
-
description:
|
|
225
|
+
description: C("timecampus.home.quickActions.teamActivity.description", "See team status")
|
|
228
226
|
},
|
|
229
227
|
{
|
|
230
|
-
label:
|
|
231
|
-
icon:
|
|
228
|
+
label: C("timecampus.home.quickActions.reports.label", "Reports"),
|
|
229
|
+
icon: l,
|
|
232
230
|
route: "/reports",
|
|
233
|
-
description:
|
|
231
|
+
description: C("timecampus.home.quickActions.reports.description", "Generate reports")
|
|
234
232
|
},
|
|
235
233
|
{
|
|
236
|
-
label:
|
|
237
|
-
icon:
|
|
234
|
+
label: C("timecampus.home.quickActions.approvals.label", "Approvals"),
|
|
235
|
+
icon: u,
|
|
238
236
|
route: "/time-approval",
|
|
239
|
-
description:
|
|
237
|
+
description: C("timecampus.home.quickActions.approvals.description", "Review timesheets")
|
|
240
238
|
}
|
|
241
|
-
].map(({ label: e, icon: t, description: n, route: r }) => /* @__PURE__ */
|
|
242
|
-
onClick: () =>
|
|
239
|
+
].map(({ label: e, icon: t, description: n, route: r }) => /* @__PURE__ */ _("button", {
|
|
240
|
+
onClick: () => w(r),
|
|
243
241
|
className: "flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border",
|
|
244
242
|
children: [
|
|
245
|
-
/* @__PURE__ */
|
|
243
|
+
/* @__PURE__ */ g("div", {
|
|
246
244
|
className: "rounded-full bg-action-primary-bg/10 p-3",
|
|
247
|
-
children: /* @__PURE__ */
|
|
245
|
+
children: /* @__PURE__ */ g(t, { className: "h-5 w-5 text-primary" })
|
|
248
246
|
}),
|
|
249
|
-
/* @__PURE__ */
|
|
247
|
+
/* @__PURE__ */ g("span", {
|
|
250
248
|
className: "text-sm font-medium text-text-primary",
|
|
251
249
|
children: e
|
|
252
250
|
}),
|
|
253
|
-
/* @__PURE__ */
|
|
251
|
+
/* @__PURE__ */ g("span", {
|
|
254
252
|
className: "text-xs text-text-secondary",
|
|
255
253
|
children: n
|
|
256
254
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\nexport const HomePage: FC = () => {\n const { t: translate } = useI18n();\n const t = (key: string, fallback: string): string => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 4 } },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HomePage.js","names":[],"sources":["../../../src/timecampus/pages/HomePage.tsx"],"sourcesContent":["import type { FC } from \"react\";\nimport {\n Clock,\n FolderOpen,\n Users,\n TrendingUp,\n CheckCircle,\n BarChart3,\n Play,\n ArrowRight,\n} from \"lucide-react\";\nimport {\n GlassCard,\n EmphasisPanel,\n AuroraBackground,\n GradientText,\n CTAOverflowMenu,\n} from \"@burdenoff/fe-libs/ui\";\nimport { StatCard } from \"../components/StatCard\";\nimport { LoadingSkeleton } from \"../components/LoadingSkeleton\";\nimport { cn, formatHours } from \"../utils/helpers\";\nimport { useTimeCampusNavigate } from \"../providers/TimeCampusProvider\";\nimport {\n useGetTimeEntryStatsQuery,\n useListTimeEntriesQuery,\n type TimeEntryFieldsFragment,\n} from \"@/generated/wspace-operations\";\nimport { useTr } from \"../hooks\";\n\nexport const HomePage: FC = () => {\n const t = useTr();\n const navigate = useTimeCampusNavigate();\n\n const { data: statsData, loading: statsLoading } =\n useGetTimeEntryStatsQuery();\n const { data: entriesData, loading: entriesLoading } =\n useListTimeEntriesQuery({\n variables: { pagination: { limit: 4 } },\n });\n\n const stats = statsData?.timeEntryStats;\n const recentEntries = entriesData?.timeEntries?.items ?? [];\n\n const headerActions = (\n <CTAOverflowMenu\n primary={{\n label: t(\"timecampus.home.actions.startTimer\", \"Start Timer\"),\n onSelect: () => navigate(\"/time/timer\"),\n icon: <Play className=\"h-4 w-4\" />,\n }}\n actions={[\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n onSelect: () => navigate(\"/projects\"),\n icon: <FolderOpen className=\"h-4 w-4\" />,\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n onSelect: () => navigate(\"/reports\"),\n icon: <BarChart3 className=\"h-4 w-4\" />,\n },\n ]}\n />\n );\n\n const renderHeader = () => (\n <div className=\"relative overflow-hidden rounded-2xl border border-border-seam bg-gloss-card p-5 sm:p-6\">\n <AuroraBackground intensity=\"subtle\" />\n <div className=\"relative flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between\">\n <div className=\"min-w-0\">\n <h1 className=\"text-2xl font-bold text-text-primary\">\n <GradientText>\n {t(\"timecampus.home.title\", \"Dashboard\")}\n </GradientText>\n </h1>\n <p className=\"mt-1 text-sm text-text-secondary\">\n {t(\n \"timecampus.home.description\",\n \"Overview of your time tracking and productivity\",\n )}\n </p>\n </div>\n {headerActions}\n </div>\n </div>\n );\n\n if (statsLoading && entriesLoading) {\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n <LoadingSkeleton variant=\"card\" count={4} />\n </div>\n );\n }\n\n return (\n <div className=\"min-h-full space-y-6 p-4 sm:p-6\">\n {renderHeader()}\n\n {/* Hero metric + supporting metrics */}\n <div className=\"grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4\">\n <StatCard\n featured\n title={t(\"timecampus.home.stats.today\", \"Today\")}\n value={formatHours(stats?.totalHours ?? 0)}\n icon={<Clock className=\"h-5 w-5\" />}\n trend={{\n value: 12.5,\n label: t(\"timecampus.home.stats.vsYesterday\", \"vs yesterday\"),\n }}\n />\n <StatCard\n title={t(\"timecampus.home.stats.billable\", \"Billable\")}\n value={formatHours(stats?.billableHours ?? 0)}\n icon={<TrendingUp className=\"h-5 w-5\" />}\n description={`${stats?.totalEntries ?? 0} ${t(\"timecampus.home.stats.entries\", \"entries\")}`}\n />\n <StatCard\n title={t(\"timecampus.home.stats.activeTimers\", \"Active Timers\")}\n value={stats?.activeTimers ?? 0}\n icon={<FolderOpen className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.currentlyRunning\",\n \"Currently running\",\n )}\n />\n <StatCard\n title={t(\"timecampus.home.stats.totalEntries\", \"Entries\")}\n value={stats?.totalEntries ?? 0}\n icon={<BarChart3 className=\"h-5 w-5\" />}\n description={t(\n \"timecampus.home.stats.totalEntriesDescription\",\n \"Total time entries\",\n )}\n />\n </div>\n\n <div className=\"grid grid-cols-1 gap-6 lg:grid-cols-3\">\n {/* Recent Time Entries */}\n <GlassCard\n treatment=\"plain\"\n className=\"border-border-seam p-6 lg:col-span-2\"\n >\n <div className=\"mb-4 flex items-center justify-between\">\n <div>\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\n \"timecampus.home.recentEntries.title\",\n \"Recent Time Entries\",\n )}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.description\",\n \"Your latest tracked activities\",\n )}\n </p>\n </div>\n <button\n onClick={() => navigate(\"/time/entries\")}\n className=\"inline-flex min-h-[2.75rem] items-center gap-1 rounded-md px-2 text-xs font-medium text-primary transition-colors hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n {t(\"timecampus.home.recentEntries.viewAll\", \"View all\")}{\" \"}\n <ArrowRight className=\"h-3 w-3\" />\n </button>\n </div>\n {entriesLoading ? (\n <LoadingSkeleton variant=\"list\" count={4} />\n ) : (\n <div className=\"flex flex-col gap-3\">\n {recentEntries.map((entry: TimeEntryFieldsFragment) => (\n <div\n key={entry.id}\n className=\"flex items-center justify-between rounded-lg border border-border-seam p-3 transition-colors hover:bg-bg-sunken/50\"\n >\n <div className=\"min-w-0 flex-1\">\n <p className=\"text-sm font-medium text-text-primary\">\n {entry.project?.name ??\n t(\n \"timecampus.home.recentEntries.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {entry.description ??\n t(\n \"timecampus.home.recentEntries.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <div className=\"ml-4 text-right\">\n <p className=\"font-mono text-sm font-medium tabular-nums text-text-primary\">\n {formatHours((entry.duration ?? 0) / 3600)}\n </p>\n <span\n className={cn(\n \"inline-flex items-center gap-1.5 rounded-full px-2 py-0.5 text-xs font-medium\",\n entry.isRunning\n ? \"bg-status-info-bg text-status-info-text\"\n : \"bg-status-success-bg text-status-success-text\",\n )}\n >\n {entry.isRunning && (\n <span className=\"h-1.5 w-1.5 rounded-full bg-status-info-text motion-safe:animate-vc-glow-pulse\" />\n )}\n {entry.isRunning\n ? t(\n \"timecampus.home.recentEntries.status.running\",\n \"running\",\n )\n : t(\n \"timecampus.home.recentEntries.status.completed\",\n \"completed\",\n )}\n </span>\n </div>\n </div>\n ))}\n {recentEntries.length === 0 && (\n <p className=\"py-4 text-center text-sm text-text-secondary\">\n {t(\n \"timecampus.home.recentEntries.empty\",\n \"No time entries yet\",\n )}\n </p>\n )}\n </div>\n )}\n </GlassCard>\n\n {/* Quick Summary — the one emphasis zone */}\n <EmphasisPanel className=\"p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.summary.title\", \"Quick Summary\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.summary.description\",\n \"Time tracking overview\",\n )}\n </p>\n </div>\n <div className=\"flex flex-col gap-4\">\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.totalHours\", \"Total Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.totalHours ?? 0)}\n </p>\n </div>\n <Clock className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.billableHours\", \"Billable Hours\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {formatHours(stats?.billableHours ?? 0)}\n </p>\n </div>\n <TrendingUp className=\"h-5 w-5 text-text-secondary\" />\n </div>\n <div className=\"flex items-center justify-between rounded-lg border border-border-seam bg-bg-surface/40 p-3\">\n <div>\n <p className=\"text-xs font-medium text-text-secondary\">\n {t(\"timecampus.home.summary.activeTimers\", \"Active Timers\")}\n </p>\n <p className=\"mt-1 text-xl font-bold tabular-nums text-text-primary\">\n {stats?.activeTimers ?? 0}\n </p>\n </div>\n <Users className=\"h-5 w-5 text-text-secondary\" />\n </div>\n </div>\n </EmphasisPanel>\n </div>\n\n {/* Quick Actions */}\n <GlassCard treatment=\"plain\" className=\"border-border-seam p-6\">\n <div className=\"mb-4\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.home.quickActions.title\", \"Quick Actions\")}\n </h3>\n <p className=\"text-xs text-text-secondary\">\n {t(\n \"timecampus.home.quickActions.description\",\n \"Frequently used features\",\n )}\n </p>\n </div>\n <div className=\"grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6\">\n {[\n {\n label: t(\n \"timecampus.home.quickActions.startTimer.label\",\n \"Start Timer\",\n ),\n icon: Play,\n route: \"/time/timer\",\n description: t(\n \"timecampus.home.quickActions.startTimer.description\",\n \"Track your time\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.viewProjects.label\",\n \"View Projects\",\n ),\n icon: FolderOpen,\n route: \"/projects\",\n description: t(\n \"timecampus.home.quickActions.viewProjects.description\",\n \"Manage projects\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.timeEntries.label\",\n \"Time Entries\",\n ),\n icon: Clock,\n route: \"/time/entries\",\n description: t(\n \"timecampus.home.quickActions.timeEntries.description\",\n \"View all entries\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.teamActivity.label\",\n \"Team Activity\",\n ),\n icon: Users,\n route: \"/team-activity\",\n description: t(\n \"timecampus.home.quickActions.teamActivity.description\",\n \"See team status\",\n ),\n },\n {\n label: t(\"timecampus.home.quickActions.reports.label\", \"Reports\"),\n icon: BarChart3,\n route: \"/reports\",\n description: t(\n \"timecampus.home.quickActions.reports.description\",\n \"Generate reports\",\n ),\n },\n {\n label: t(\n \"timecampus.home.quickActions.approvals.label\",\n \"Approvals\",\n ),\n icon: CheckCircle,\n route: \"/time-approval\",\n description: t(\n \"timecampus.home.quickActions.approvals.description\",\n \"Review timesheets\",\n ),\n },\n ].map(({ label, icon: Icon, description, route }) => (\n <button\n key={label}\n onClick={() => navigate(route)}\n className=\"flex min-h-[2.75rem] flex-col items-center gap-2 rounded-lg border border-border-seam bg-bg-surface p-4 text-center transition-colors hover:bg-bg-sunken/50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-action-primary-border\"\n >\n <div className=\"rounded-full bg-action-primary-bg/10 p-3\">\n <Icon className=\"h-5 w-5 text-primary\" />\n </div>\n <span className=\"text-sm font-medium text-text-primary\">\n {label}\n </span>\n <span className=\"text-xs text-text-secondary\">{description}</span>\n </button>\n ))}\n </div>\n </GlassCard>\n </div>\n );\n};\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"TimerPage.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"names":[],"mappings":"AA4BA,wBAAgB,SAAS,gCAqaxB"}
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { useTimeCampusNavigate as e } from "../../providers/TimeCampusProvider.js";
|
|
2
|
-
import { formatTimerDuration as t, getInitials as
|
|
3
|
-
import { LoadingSkeleton as
|
|
4
|
-
import { GetTimeEntryStatsDocument as
|
|
5
|
-
import {
|
|
6
|
-
import
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
2
|
+
import { formatTimerDuration as t, getInitials as n } from "../../utils/helpers.js";
|
|
3
|
+
import { LoadingSkeleton as r } from "../../components/LoadingSkeleton.js";
|
|
4
|
+
import { GetTimeEntryStatsDocument as ee, ListRunningTimersDocument as i, ListTimeEntriesDocument as a, useDeleteTimeEntryMutation as o, useListProjectsQuery as s, useListRunningTimersQuery as c, useStartTimerMutation as l, useStopTimerMutation as u } from "../../../generated/wspace-operations.js";
|
|
5
|
+
import { useTr as d } from "../../shared/hooks/useTr.js";
|
|
6
|
+
import "../../hooks/index.js";
|
|
7
|
+
import { PageLayout as f } from "../../components/PageLayout.js";
|
|
8
|
+
import { useTimeCampusStore as te } from "../../store/timecampusStore.js";
|
|
9
|
+
import { StatusBadge as ne } from "../../components/StatusBadge.js";
|
|
10
|
+
import { EmptyState as re } from "../../components/EmptyState.js";
|
|
9
11
|
import { Modal as p } from "../../components/Modal.js";
|
|
10
12
|
import { useEffect as m, useState as h } from "react";
|
|
11
13
|
import { Play as g, RotateCcw as _, Square as v } from "lucide-react";
|
|
@@ -13,14 +15,14 @@ import { Fragment as y, jsx as b, jsxs as x } from "react/jsx-runtime";
|
|
|
13
15
|
import { GlassCard as S, NextSteps as C } from "@burdenoff/fe-libs/ui";
|
|
14
16
|
//#region src/timecampus/pages/time/TimerPage.tsx
|
|
15
17
|
function w() {
|
|
16
|
-
let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } =
|
|
17
|
-
{ query:
|
|
18
|
+
let { activeTimer: w, tickTimer: T, syncTimerFromServer: E } = te(), D = e(), O = d(), [ie, k] = h(!1), [A, j] = h(""), [M, N] = h(""), [P, F] = h(!1), [I, L] = h(null), { data: R, loading: z, refetch: B } = c({ fetchPolicy: "cache-and-network" }), { data: V } = s({ variables: { pagination: { limit: 50 } } }), H = V?.projects?.items ?? [], U = R?.runningTimers ?? [], W = U[0] ?? null, G = [
|
|
19
|
+
{ query: i },
|
|
18
20
|
{
|
|
19
|
-
query:
|
|
21
|
+
query: a,
|
|
20
22
|
variables: { pagination: { limit: 50 } }
|
|
21
23
|
},
|
|
22
|
-
{ query:
|
|
23
|
-
], [K, { loading: q, error: J }] =
|
|
24
|
+
{ query: ee }
|
|
25
|
+
], [K, { loading: q, error: J }] = l({ refetchQueries: G }), [Y, { loading: X }] = u({ refetchQueries: G }), [ae, { loading: Z }] = o({ refetchQueries: G });
|
|
24
26
|
m(() => {
|
|
25
27
|
W ? E({
|
|
26
28
|
timeEntryId: W.timeEntry.id,
|
|
@@ -38,7 +40,7 @@ function w() {
|
|
|
38
40
|
let e = setInterval(() => T(), 1e3);
|
|
39
41
|
return () => clearInterval(e);
|
|
40
42
|
}, [w?.isRunning, T]);
|
|
41
|
-
let
|
|
43
|
+
let oe = async () => {
|
|
42
44
|
await K({ variables: { input: {
|
|
43
45
|
description: A.trim() || void 0,
|
|
44
46
|
projectId: M || void 0,
|
|
@@ -47,13 +49,13 @@ function w() {
|
|
|
47
49
|
} } }), k(!1), j(""), N(""), F(!1), await B();
|
|
48
50
|
}, Q = async () => {
|
|
49
51
|
W && (await Y({ variables: { id: W.timeEntry.id } }), await B());
|
|
50
|
-
},
|
|
51
|
-
I && (await Y({ variables: { id: I.timeEntry.id } }), await
|
|
52
|
+
}, se = async () => {
|
|
53
|
+
I && (await Y({ variables: { id: I.timeEntry.id } }), await ae({ variables: { id: I.timeEntry.id } }), L(null), await B());
|
|
52
54
|
}, $ = !!W;
|
|
53
|
-
return /* @__PURE__ */ x(
|
|
54
|
-
title: "Timer",
|
|
55
|
-
description: "Focus timer for deep work sessions",
|
|
56
|
-
purpose: "A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.",
|
|
55
|
+
return /* @__PURE__ */ x(f, {
|
|
56
|
+
title: O("timecampus.timer.title", "Timer"),
|
|
57
|
+
description: O("timecampus.timer.description", "Focus timer for deep work sessions"),
|
|
58
|
+
purpose: O("timecampus.timer.purpose", "A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later."),
|
|
57
59
|
children: [
|
|
58
60
|
/* @__PURE__ */ x("div", {
|
|
59
61
|
className: "mx-auto max-w-lg",
|
|
@@ -64,20 +66,20 @@ function w() {
|
|
|
64
66
|
steps: [
|
|
65
67
|
{
|
|
66
68
|
id: "start",
|
|
67
|
-
label: $ ? "Stop the running timer" : "Start the timer",
|
|
68
|
-
description: $ ? "End this session and save it as a time entry." : "Begin tracking the task you're working on right now.",
|
|
69
|
+
label: $ ? O("timecampus.timer.nextSteps.stopRunning", "Stop the running timer") : O("timecampus.timer.nextSteps.start", "Start the timer"),
|
|
70
|
+
description: $ ? O("timecampus.timer.nextSteps.stopDescription", "End this session and save it as a time entry.") : O("timecampus.timer.nextSteps.startDescription", "Begin tracking the task you're working on right now."),
|
|
69
71
|
onClick: () => $ ? Q() : k(!0)
|
|
70
72
|
},
|
|
71
73
|
{
|
|
72
74
|
id: "assign-project",
|
|
73
|
-
label: "Assign work to a project",
|
|
74
|
-
description: "Pick a project so this time bills correctly.",
|
|
75
|
+
label: O("timecampus.timer.nextSteps.assignProject", "Assign work to a project"),
|
|
76
|
+
description: O("timecampus.timer.nextSteps.assignDescription", "Pick a project so this time bills correctly."),
|
|
75
77
|
onClick: () => D("/projects")
|
|
76
78
|
},
|
|
77
79
|
{
|
|
78
80
|
id: "review-entries",
|
|
79
|
-
label: "Review your time entries",
|
|
80
|
-
description: "Check and tidy everything you've logged.",
|
|
81
|
+
label: O("timecampus.timer.nextSteps.review", "Review your time entries"),
|
|
82
|
+
description: O("timecampus.timer.nextSteps.reviewDescription", "Check and tidy everything you've logged."),
|
|
81
83
|
onClick: () => D("/time/entries")
|
|
82
84
|
}
|
|
83
85
|
]
|
|
@@ -89,7 +91,7 @@ function w() {
|
|
|
89
91
|
children: [
|
|
90
92
|
/* @__PURE__ */ b("p", {
|
|
91
93
|
className: "font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl",
|
|
92
|
-
children: w ? t(w.seconds) : "00:00:00"
|
|
94
|
+
children: w ? t(w.seconds) : O("timecampus.timer.elapsedDefault", "00:00:00")
|
|
93
95
|
}),
|
|
94
96
|
w?.description && /* @__PURE__ */ b("p", {
|
|
95
97
|
className: "mt-2 text-sm text-text-secondary",
|
|
@@ -101,18 +103,18 @@ function w() {
|
|
|
101
103
|
onClick: Q,
|
|
102
104
|
disabled: X,
|
|
103
105
|
className: "inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50",
|
|
104
|
-
children: [/* @__PURE__ */ b(v, { className: "h-4 w-4" }), X ? "Stopping..." : "Stop"]
|
|
106
|
+
children: [/* @__PURE__ */ b(v, { className: "h-4 w-4" }), X ? O("timecampus.timer.stopping", "Stopping...") : O("timecampus.timer.stop", "Stop")]
|
|
105
107
|
}) : /* @__PURE__ */ x("button", {
|
|
106
108
|
onClick: () => k(!0),
|
|
107
109
|
disabled: q,
|
|
108
110
|
className: "inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
109
|
-
children: [/* @__PURE__ */ b(g, { className: "h-4 w-4" }), "Start"]
|
|
111
|
+
children: [/* @__PURE__ */ b(g, { className: "h-4 w-4" }), O("timecampus.timer.start", "Start")]
|
|
110
112
|
}), /* @__PURE__ */ x("button", {
|
|
111
113
|
onClick: () => W && L(W),
|
|
112
114
|
disabled: !$ || Z,
|
|
113
115
|
className: "inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50",
|
|
114
|
-
title: "Discard the running timer without saving",
|
|
115
|
-
children: [/* @__PURE__ */ b(_, { className: "h-4 w-4" }), "Reset"]
|
|
116
|
+
title: O("timecampus.timer.resetTooltip", "Discard the running timer without saving"),
|
|
117
|
+
children: [/* @__PURE__ */ b(_, { className: "h-4 w-4" }), O("timecampus.timer.reset", "Reset")]
|
|
116
118
|
})]
|
|
117
119
|
})
|
|
118
120
|
]
|
|
@@ -124,19 +126,19 @@ function w() {
|
|
|
124
126
|
className: "mb-4 flex items-center justify-between",
|
|
125
127
|
children: [/* @__PURE__ */ b("h3", {
|
|
126
128
|
className: "text-sm font-semibold text-text-primary",
|
|
127
|
-
children: "Running Timers"
|
|
128
|
-
}), U.length > 0 && /* @__PURE__ */ b(
|
|
129
|
-
label:
|
|
129
|
+
children: O("timecampus.timer.runningTimers.title", "Running Timers")
|
|
130
|
+
}), U.length > 0 && /* @__PURE__ */ b(ne, {
|
|
131
|
+
label: O("timecampus.timer.runningTimers.active", "{{count}} active", { count: U.length }),
|
|
130
132
|
variant: "success",
|
|
131
133
|
dot: !0
|
|
132
134
|
})]
|
|
133
|
-
}), z && U.length === 0 ? /* @__PURE__ */ b(
|
|
135
|
+
}), z && U.length === 0 ? /* @__PURE__ */ b(r, {
|
|
134
136
|
variant: "list",
|
|
135
137
|
count: 2
|
|
136
|
-
}) : U.length === 0 ? /* @__PURE__ */ b(
|
|
138
|
+
}) : U.length === 0 ? /* @__PURE__ */ b(re, {
|
|
137
139
|
illustration: "empty-sessions",
|
|
138
|
-
title: "No active timers on the server",
|
|
139
|
-
description: "Timers you start sync here so you can pick them up on any device."
|
|
140
|
+
title: O("timecampus.timer.runningTimers.emptyTitle", "No active timers on the server"),
|
|
141
|
+
description: O("timecampus.timer.runningTimers.emptyDescription", "Timers you start sync here so you can pick them up on any device.")
|
|
140
142
|
}) : /* @__PURE__ */ b("div", {
|
|
141
143
|
className: "flex flex-col gap-2",
|
|
142
144
|
children: U.map((e) => /* @__PURE__ */ x("div", {
|
|
@@ -144,16 +146,16 @@ function w() {
|
|
|
144
146
|
children: [
|
|
145
147
|
/* @__PURE__ */ b("span", {
|
|
146
148
|
className: "flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary",
|
|
147
|
-
children:
|
|
149
|
+
children: n(e.timeEntry.project?.name ?? O("timecampus.timer.runningTimers.noProject", "No Project"))
|
|
148
150
|
}),
|
|
149
151
|
/* @__PURE__ */ x("div", {
|
|
150
152
|
className: "min-w-0 flex-1",
|
|
151
153
|
children: [/* @__PURE__ */ b("p", {
|
|
152
154
|
className: "truncate text-sm font-medium text-text-primary",
|
|
153
|
-
children: e.timeEntry.project?.name ?? "No Project"
|
|
155
|
+
children: e.timeEntry.project?.name ?? O("timecampus.timer.runningTimers.noProject", "No Project")
|
|
154
156
|
}), /* @__PURE__ */ b("p", {
|
|
155
157
|
className: "truncate text-xs text-text-secondary",
|
|
156
|
-
children: e.timeEntry.description ?? "No description"
|
|
158
|
+
children: e.timeEntry.description ?? O("timecampus.timer.runningTimers.noDescription", "No description")
|
|
157
159
|
})]
|
|
158
160
|
}),
|
|
159
161
|
/* @__PURE__ */ b("p", {
|
|
@@ -167,46 +169,46 @@ function w() {
|
|
|
167
169
|
]
|
|
168
170
|
}),
|
|
169
171
|
/* @__PURE__ */ x(p, {
|
|
170
|
-
open:
|
|
172
|
+
open: ie,
|
|
171
173
|
onClose: () => k(!1),
|
|
172
|
-
title: "Start a timer",
|
|
173
|
-
description: "Track the task you're working on right now.",
|
|
174
|
+
title: O("timecampus.timer.modal.startTitle", "Start a timer"),
|
|
175
|
+
description: O("timecampus.timer.modal.startDescription", "Track the task you're working on right now."),
|
|
174
176
|
footer: /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("button", {
|
|
175
177
|
onClick: () => k(!1),
|
|
176
178
|
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
177
|
-
children: "Cancel"
|
|
179
|
+
children: O("timecampus.timer.modal.cancel", "Cancel")
|
|
178
180
|
}), /* @__PURE__ */ b("button", {
|
|
179
|
-
onClick:
|
|
181
|
+
onClick: oe,
|
|
180
182
|
disabled: q,
|
|
181
183
|
className: "rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50",
|
|
182
|
-
children: q ? "Starting..." : "Start Timer"
|
|
184
|
+
children: q ? O("timecampus.timer.modal.starting", "Starting...") : O("timecampus.timer.modal.startTimer", "Start Timer")
|
|
183
185
|
})] }),
|
|
184
186
|
children: [J && /* @__PURE__ */ b("div", {
|
|
185
187
|
className: "mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text",
|
|
186
|
-
children: "Failed to start the timer. Please try again."
|
|
188
|
+
children: O("timecampus.timer.modal.startError", "Failed to start the timer. Please try again.")
|
|
187
189
|
}), /* @__PURE__ */ x("div", {
|
|
188
190
|
className: "space-y-4",
|
|
189
191
|
children: [
|
|
190
192
|
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
191
193
|
className: "block text-sm font-medium text-text-primary",
|
|
192
|
-
children: "What are you working on?"
|
|
194
|
+
children: O("timecampus.timer.modal.descriptionLabel", "What are you working on?")
|
|
193
195
|
}), /* @__PURE__ */ b("input", {
|
|
194
196
|
type: "text",
|
|
195
|
-
placeholder: "Description",
|
|
197
|
+
placeholder: O("timecampus.timer.modal.descriptionPlaceholder", "Description"),
|
|
196
198
|
value: A,
|
|
197
199
|
onChange: (e) => j(e.target.value),
|
|
198
200
|
className: "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary"
|
|
199
201
|
})] }),
|
|
200
202
|
/* @__PURE__ */ x("div", { children: [/* @__PURE__ */ b("label", {
|
|
201
203
|
className: "block text-sm font-medium text-text-primary",
|
|
202
|
-
children: "Project"
|
|
204
|
+
children: O("timecampus.timer.modal.projectLabel", "Project")
|
|
203
205
|
}), /* @__PURE__ */ x("select", {
|
|
204
206
|
value: M,
|
|
205
207
|
onChange: (e) => N(e.target.value),
|
|
206
208
|
className: "mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary",
|
|
207
209
|
children: [/* @__PURE__ */ b("option", {
|
|
208
210
|
value: "",
|
|
209
|
-
children: "No Project"
|
|
211
|
+
children: O("timecampus.timer.modal.noProject", "No Project")
|
|
210
212
|
}), H.map((e) => /* @__PURE__ */ b("option", {
|
|
211
213
|
value: e.id,
|
|
212
214
|
children: e.name
|
|
@@ -219,7 +221,7 @@ function w() {
|
|
|
219
221
|
checked: P,
|
|
220
222
|
onChange: (e) => F(e.target.checked),
|
|
221
223
|
className: "h-4 w-4 rounded border-border-subtle"
|
|
222
|
-
}), "Billable"]
|
|
224
|
+
}), O("timecampus.timer.modal.billable", "Billable")]
|
|
223
225
|
})
|
|
224
226
|
]
|
|
225
227
|
})]
|
|
@@ -227,29 +229,29 @@ function w() {
|
|
|
227
229
|
/* @__PURE__ */ b(p, {
|
|
228
230
|
open: !!I,
|
|
229
231
|
onClose: () => L(null),
|
|
230
|
-
title: "Discard running timer?",
|
|
231
|
-
description: "This stops the timer and deletes the in-progress entry. It cannot be undone.",
|
|
232
|
+
title: O("timecampus.timer.modal.discardTitle", "Discard running timer?"),
|
|
233
|
+
description: O("timecampus.timer.modal.discardDescription", "This stops the timer and deletes the in-progress entry. It cannot be undone."),
|
|
232
234
|
footer: /* @__PURE__ */ x(y, { children: [/* @__PURE__ */ b("button", {
|
|
233
235
|
onClick: () => L(null),
|
|
234
236
|
className: "rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50",
|
|
235
|
-
children: "Cancel"
|
|
237
|
+
children: O("timecampus.timer.modal.cancel", "Cancel")
|
|
236
238
|
}), /* @__PURE__ */ b("button", {
|
|
237
|
-
onClick:
|
|
239
|
+
onClick: se,
|
|
238
240
|
disabled: X || Z,
|
|
239
241
|
className: "rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50",
|
|
240
|
-
children: X || Z ? "Discarding..." : "Discard"
|
|
242
|
+
children: X || Z ? O("timecampus.timer.modal.discarding", "Discarding...") : O("timecampus.timer.modal.discard", "Discard")
|
|
241
243
|
})] }),
|
|
242
244
|
children: /* @__PURE__ */ x("p", {
|
|
243
245
|
className: "text-sm text-text-secondary",
|
|
244
246
|
children: [
|
|
245
|
-
"The timer for",
|
|
247
|
+
O("timecampus.timer.modal.discardMessagePrefix", "The timer for"),
|
|
246
248
|
" ",
|
|
247
249
|
/* @__PURE__ */ b("span", {
|
|
248
250
|
className: "font-medium text-text-primary",
|
|
249
|
-
children: I?.timeEntry.project?.name ?? I?.timeEntry.description ?? "this session"
|
|
251
|
+
children: I?.timeEntry.project?.name ?? I?.timeEntry.description ?? O("timecampus.timer.modal.sessionFallback", "this session")
|
|
250
252
|
}),
|
|
251
253
|
" ",
|
|
252
|
-
"will be discarded."
|
|
254
|
+
O("timecampus.timer.modal.discardMessageSuffix", "will be discarded.")
|
|
253
255
|
]
|
|
254
256
|
})
|
|
255
257
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title=\"Timer\"\n description=\"Focus timer for deep work sessions\"\n purpose=\"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\"\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning ? \"Stop the running timer\" : \"Start the timer\",\n description: isRunning\n ? \"End this session and save it as a time entry.\"\n : \"Begin tracking the task you're working on right now.\",\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: \"Assign work to a project\",\n description: \"Pick a project so this time bills correctly.\",\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: \"Review your time entries\",\n description: \"Check and tidy everything you've logged.\",\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : \"00:00:00\"}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping ? \"Stopping...\" : \"Stop\"}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n Start\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title=\"Discard the running timer without saving\"\n >\n <RotateCcw className=\"h-4 w-4\" />\n Reset\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n Running Timers\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={`${runningTimers.length} active`}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title=\"No active timers on the server\"\n description=\"Timers you start sync here so you can pick them up on any device.\"\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(timer.timeEntry.project?.name ?? \"No Project\")}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ?? \"No Project\"}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ?? \"No description\"}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title=\"Start a timer\"\n description=\"Track the task you're working on right now.\"\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {starting ? \"Starting...\" : \"Start Timer\"}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n Failed to start the timer. Please try again.\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n What are you working on?\n </label>\n <input\n type=\"text\"\n placeholder=\"Description\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n Project\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">No Project</option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n Billable\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title=\"Discard running timer?\"\n description=\"This stops the timer and deletes the in-progress entry. It cannot be undone.\"\n footer={\n <>\n <button\n onClick={() => setResetTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n Cancel\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting ? \"Discarding...\" : \"Discard\"}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n The timer for{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n \"this session\"}\n </span>{\" \"}\n will be discarded.\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TimerPage.js","names":[],"sources":["../../../../src/timecampus/pages/time/TimerPage.tsx"],"sourcesContent":["import { useEffect, useState } from \"react\";\nimport { Play, Square, RotateCcw } from \"lucide-react\";\nimport { GlassCard, NextSteps } from \"@burdenoff/fe-libs/ui\";\nimport { PageLayout } from \"../../components/PageLayout\";\nimport { Modal } from \"../../components/Modal\";\nimport { EmptyState } from \"../../components/EmptyState\";\nimport { LoadingSkeleton } from \"../../components/LoadingSkeleton\";\nimport { StatusBadge } from \"../../components/StatusBadge\";\nimport { useTimeCampusNavigate } from \"../../providers/TimeCampusProvider\";\nimport { formatTimerDuration, getInitials } from \"../../utils/helpers\";\nimport { useTimeCampusStore } from \"../../store/timecampusStore\";\nimport { useTr } from \"../../hooks\";\nimport {\n useListRunningTimersQuery,\n useStartTimerMutation,\n useStopTimerMutation,\n useDeleteTimeEntryMutation,\n useListProjectsQuery,\n ListRunningTimersDocument,\n ListTimeEntriesDocument,\n GetTimeEntryStatsDocument,\n type ListRunningTimersQuery,\n type ListProjectsQuery,\n} from \"@/generated/wspace-operations\";\n\ntype RunningTimerItem = ListRunningTimersQuery[\"runningTimers\"][number];\ntype ProjectOption = ListProjectsQuery[\"projects\"][\"items\"][number];\n\nexport function TimerPage() {\n const { activeTimer, tickTimer, syncTimerFromServer } = useTimeCampusStore();\n const navigate = useTimeCampusNavigate();\n const t = useTr();\n\n const [startOpen, setStartOpen] = useState(false);\n const [description, setDescription] = useState(\"\");\n const [projectId, setProjectId] = useState(\"\");\n const [billable, setBillable] = useState(false);\n const [resetTarget, setResetTarget] = useState<RunningTimerItem | null>(null);\n\n const {\n data: runningData,\n loading: runningLoading,\n refetch: refetchRunning,\n } = useListRunningTimersQuery({ fetchPolicy: \"cache-and-network\" });\n\n const { data: projectsData } = useListProjectsQuery({\n variables: { pagination: { limit: 50 } },\n });\n const projects: ProjectOption[] = projectsData?.projects?.items ?? [];\n\n const runningTimers = runningData?.runningTimers ?? [];\n const serverTimer = runningTimers[0] ?? null;\n\n const refetchAll = [\n { query: ListRunningTimersDocument },\n {\n query: ListTimeEntriesDocument,\n variables: { pagination: { limit: 50 } },\n },\n { query: GetTimeEntryStatsDocument },\n ];\n\n const [startTimerMutation, { loading: starting, error: startError }] =\n useStartTimerMutation({ refetchQueries: refetchAll });\n const [stopTimerMutation, { loading: stopping }] = useStopTimerMutation({\n refetchQueries: refetchAll,\n });\n const [deleteTimeEntry, { loading: deleting }] = useDeleteTimeEntryMutation({\n refetchQueries: refetchAll,\n });\n\n // Backend is the source of truth for whether a timer is running — mirror the\n // server running timer into the local display store whenever it changes.\n useEffect(() => {\n if (serverTimer) {\n syncTimerFromServer({\n timeEntryId: serverTimer.timeEntry.id,\n elapsedSeconds: serverTimer.elapsedSeconds,\n projectId: serverTimer.timeEntry.projectId ?? undefined,\n taskId: serverTimer.timeEntry.taskId ?? undefined,\n description: serverTimer.timeEntry.description ?? undefined,\n });\n } else if (!runningLoading) {\n syncTimerFromServer(null);\n }\n // Only re-sync when the server's running timer identity/elapsed changes.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [serverTimer?.timeEntry.id, serverTimer?.elapsedSeconds, runningLoading]);\n\n // Local ticking display for the live seconds counter.\n useEffect(() => {\n if (!activeTimer?.isRunning) return;\n const interval = setInterval(() => tickTimer(), 1000);\n return () => clearInterval(interval);\n }, [activeTimer?.isRunning, tickTimer]);\n\n const handleStart = async () => {\n await startTimerMutation({\n variables: {\n input: {\n description: description.trim() || undefined,\n projectId: projectId || undefined,\n billable,\n source: \"timer\",\n },\n },\n });\n setStartOpen(false);\n setDescription(\"\");\n setProjectId(\"\");\n setBillable(false);\n await refetchRunning();\n };\n\n const handleStop = async () => {\n if (!serverTimer) return;\n await stopTimerMutation({ variables: { id: serverTimer.timeEntry.id } });\n await refetchRunning();\n };\n\n const handleReset = async () => {\n if (!resetTarget) return;\n // Discard the in-progress session: stop it, then delete the entry.\n await stopTimerMutation({ variables: { id: resetTarget.timeEntry.id } });\n await deleteTimeEntry({ variables: { id: resetTarget.timeEntry.id } });\n setResetTarget(null);\n await refetchRunning();\n };\n\n const isRunning = Boolean(serverTimer);\n\n return (\n <PageLayout\n title={t(\"timecampus.timer.title\", \"Timer\")}\n description={t(\n \"timecampus.timer.description\",\n \"Focus timer for deep work sessions\",\n )}\n purpose={t(\n \"timecampus.timer.purpose\",\n \"A single, distraction-free stopwatch for the task in front of you — start it when you begin focused work and it logs the session as a time entry you can attribute to a project later.\",\n )}\n >\n <div className=\"mx-auto max-w-lg\">\n <NextSteps\n storageKey=\"timecampus-timer\"\n className=\"mb-6\"\n steps={[\n {\n id: \"start\",\n label: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopRunning\",\n \"Stop the running timer\",\n )\n : t(\"timecampus.timer.nextSteps.start\", \"Start the timer\"),\n description: isRunning\n ? t(\n \"timecampus.timer.nextSteps.stopDescription\",\n \"End this session and save it as a time entry.\",\n )\n : t(\n \"timecampus.timer.nextSteps.startDescription\",\n \"Begin tracking the task you're working on right now.\",\n ),\n onClick: () => (isRunning ? handleStop() : setStartOpen(true)),\n },\n {\n id: \"assign-project\",\n label: t(\n \"timecampus.timer.nextSteps.assignProject\",\n \"Assign work to a project\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.assignDescription\",\n \"Pick a project so this time bills correctly.\",\n ),\n onClick: () => navigate(\"/projects\"),\n },\n {\n id: \"review-entries\",\n label: t(\n \"timecampus.timer.nextSteps.review\",\n \"Review your time entries\",\n ),\n description: t(\n \"timecampus.timer.nextSteps.reviewDescription\",\n \"Check and tidy everything you've logged.\",\n ),\n onClick: () => navigate(\"/time/entries\"),\n },\n ]}\n />\n <GlassCard treatment=\"glass\" glow className=\"p-6 text-center sm:p-8\">\n <p className=\"font-mono text-5xl font-bold tabular-nums text-text-primary sm:text-6xl\">\n {activeTimer\n ? formatTimerDuration(activeTimer.seconds)\n : t(\"timecampus.timer.elapsedDefault\", \"00:00:00\")}\n </p>\n {activeTimer?.description && (\n <p className=\"mt-2 text-sm text-text-secondary\">\n {activeTimer.description}\n </p>\n )}\n <div className=\"mt-6 flex flex-wrap items-center justify-center gap-3\">\n {isRunning ? (\n <button\n onClick={handleStop}\n disabled={stopping}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-danger-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n <Square className=\"h-4 w-4\" />\n {stopping\n ? t(\"timecampus.timer.stopping\", \"Stopping...\")\n : t(\"timecampus.timer.stop\", \"Stop\")}\n </button>\n ) : (\n <button\n onClick={() => setStartOpen(true)}\n disabled={starting}\n className=\"inline-flex items-center gap-2 rounded-md bg-action-primary-bg px-6 py-3 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" />\n {t(\"timecampus.timer.start\", \"Start\")}\n </button>\n )}\n <button\n onClick={() => serverTimer && setResetTarget(serverTimer)}\n disabled={!isRunning || deleting}\n className=\"inline-flex items-center gap-2 rounded-md border border-border-subtle px-4 py-3 text-sm font-medium text-text-primary hover:bg-bg-sunken/50 disabled:opacity-50\"\n title={t(\n \"timecampus.timer.resetTooltip\",\n \"Discard the running timer without saving\",\n )}\n >\n <RotateCcw className=\"h-4 w-4\" />\n {t(\"timecampus.timer.reset\", \"Reset\")}\n </button>\n </div>\n </GlassCard>\n\n {/* Server-side running timers */}\n <GlassCard treatment=\"glass\" className=\"mt-6 p-6\">\n <div className=\"mb-4 flex items-center justify-between\">\n <h3 className=\"text-sm font-semibold text-text-primary\">\n {t(\"timecampus.timer.runningTimers.title\", \"Running Timers\")}\n </h3>\n {runningTimers.length > 0 && (\n <StatusBadge\n label={t(\n \"timecampus.timer.runningTimers.active\",\n \"{{count}} active\",\n { count: runningTimers.length },\n )}\n variant=\"success\"\n dot\n />\n )}\n </div>\n {runningLoading && runningTimers.length === 0 ? (\n <LoadingSkeleton variant=\"list\" count={2} />\n ) : runningTimers.length === 0 ? (\n <EmptyState\n illustration=\"empty-sessions\"\n title={t(\n \"timecampus.timer.runningTimers.emptyTitle\",\n \"No active timers on the server\",\n )}\n description={t(\n \"timecampus.timer.runningTimers.emptyDescription\",\n \"Timers you start sync here so you can pick them up on any device.\",\n )}\n />\n ) : (\n <div className=\"flex flex-col gap-2\">\n {runningTimers.map((timer: RunningTimerItem) => (\n <div\n key={timer.timeEntry.id}\n className=\"flex items-center gap-3 rounded-lg border border-border-subtle bg-bg-surface p-3\"\n >\n <span className=\"flex h-9 w-9 shrink-0 items-center justify-center rounded-lg bg-action-primary-bg/10 text-xs font-semibold text-primary\">\n {getInitials(\n timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n ),\n )}\n </span>\n <div className=\"min-w-0 flex-1\">\n <p className=\"truncate text-sm font-medium text-text-primary\">\n {timer.timeEntry.project?.name ??\n t(\n \"timecampus.timer.runningTimers.noProject\",\n \"No Project\",\n )}\n </p>\n <p className=\"truncate text-xs text-text-secondary\">\n {timer.timeEntry.description ??\n t(\n \"timecampus.timer.runningTimers.noDescription\",\n \"No description\",\n )}\n </p>\n </div>\n <p className=\"ml-2 shrink-0 font-mono text-sm font-medium text-text-primary\">\n {formatTimerDuration(timer.elapsedSeconds)}\n </p>\n </div>\n ))}\n </div>\n )}\n </GlassCard>\n </div>\n\n {/* Start timer modal */}\n <Modal\n open={startOpen}\n onClose={() => setStartOpen(false)}\n title={t(\"timecampus.timer.modal.startTitle\", \"Start a timer\")}\n description={t(\n \"timecampus.timer.modal.startDescription\",\n \"Track the task you're working on right now.\",\n )}\n footer={\n <>\n <button\n onClick={() => setStartOpen(false)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleStart}\n disabled={starting}\n className=\"rounded-md bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-primary-bg/90 disabled:opacity-50\"\n >\n {starting\n ? t(\"timecampus.timer.modal.starting\", \"Starting...\")\n : t(\"timecampus.timer.modal.startTimer\", \"Start Timer\")}\n </button>\n </>\n }\n >\n {startError && (\n <div className=\"mb-4 rounded-lg border border-status-error-bg/40 bg-status-error-bg p-3 text-sm text-status-error-text\">\n {t(\n \"timecampus.timer.modal.startError\",\n \"Failed to start the timer. Please try again.\",\n )}\n </div>\n )}\n <div className=\"space-y-4\">\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\n \"timecampus.timer.modal.descriptionLabel\",\n \"What are you working on?\",\n )}\n </label>\n <input\n type=\"text\"\n placeholder={t(\n \"timecampus.timer.modal.descriptionPlaceholder\",\n \"Description\",\n )}\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary\"\n />\n </div>\n <div>\n <label className=\"block text-sm font-medium text-text-primary\">\n {t(\"timecampus.timer.modal.projectLabel\", \"Project\")}\n </label>\n <select\n value={projectId}\n onChange={(e) => setProjectId(e.target.value)}\n className=\"mt-1 w-full rounded-md border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary\"\n >\n <option value=\"\">\n {t(\"timecampus.timer.modal.noProject\", \"No Project\")}\n </option>\n {projects.map((project) => (\n <option key={project.id} value={project.id}>\n {project.name}\n </option>\n ))}\n </select>\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={billable}\n onChange={(e) => setBillable(e.target.checked)}\n className=\"h-4 w-4 rounded border-border-subtle\"\n />\n {t(\"timecampus.timer.modal.billable\", \"Billable\")}\n </label>\n </div>\n </Modal>\n\n {/* Reset / discard confirm modal */}\n <Modal\n open={Boolean(resetTarget)}\n onClose={() => setResetTarget(null)}\n title={t(\n \"timecampus.timer.modal.discardTitle\",\n \"Discard running timer?\",\n )}\n description={t(\n \"timecampus.timer.modal.discardDescription\",\n \"This stops the timer and deletes the in-progress entry. It cannot be undone.\",\n )}\n footer={\n <>\n <button\n onClick={() => setResetTarget(null)}\n className=\"rounded-md border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken/50\"\n >\n {t(\"timecampus.timer.modal.cancel\", \"Cancel\")}\n </button>\n <button\n onClick={handleReset}\n disabled={stopping || deleting}\n className=\"rounded-md bg-action-danger-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:bg-action-danger-bg/90 disabled:opacity-50\"\n >\n {stopping || deleting\n ? t(\"timecampus.timer.modal.discarding\", \"Discarding...\")\n : t(\"timecampus.timer.modal.discard\", \"Discard\")}\n </button>\n </>\n }\n >\n <p className=\"text-sm text-text-secondary\">\n {t(\"timecampus.timer.modal.discardMessagePrefix\", \"The timer for\")}{\" \"}\n <span className=\"font-medium text-text-primary\">\n {resetTarget?.timeEntry.project?.name ??\n resetTarget?.timeEntry.description ??\n t(\"timecampus.timer.modal.sessionFallback\", \"this session\")}\n </span>{\" \"}\n {t(\n \"timecampus.timer.modal.discardMessageSuffix\",\n \"will be discarded.\",\n )}\n </p>\n </Modal>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useTr — translation hook with fallback semantics.
|
|
3
|
+
*
|
|
4
|
+
* Wraps `useI18n().t()` so that when a key has no translation (i.e. the
|
|
5
|
+
* provider returns the key itself), the caller's English fallback is used
|
|
6
|
+
* instead. This keeps consumer call sites consistent and avoids leaking
|
|
7
|
+
* untranslated keys into the UI.
|
|
8
|
+
*
|
|
9
|
+
* Supports optional interpolation params using `{{key}}` placeholders.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* const tr = useTr();
|
|
13
|
+
* return <h1>{tr('timecampus.timer.title', 'Timer')}</h1>;
|
|
14
|
+
* return <p>{tr('timecampus.timer.activeCount', '{{count}} active', { count: 5 })}</p>;
|
|
15
|
+
*/
|
|
16
|
+
export declare function useTr(): (key: string, fallback: string, params?: Record<string, string | number>) => string;
|
|
17
|
+
//# sourceMappingURL=useTr.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.d.ts","sourceRoot":"","sources":["../../../../src/timecampus/shared/hooks/useTr.ts"],"names":[],"mappings":"AAGA;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,KAAK,IAAI,CACvB,GAAG,EAAE,MAAM,EACX,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC,KACrC,MAAM,CAmBV"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { useCallback as e } from "react";
|
|
2
|
+
import { useI18n as t } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
3
|
+
//#region src/timecampus/shared/hooks/useTr.ts
|
|
4
|
+
function n() {
|
|
5
|
+
let { t: n } = t();
|
|
6
|
+
return e((e, t, r) => {
|
|
7
|
+
let i = n(e), a = i === e ? t : i;
|
|
8
|
+
return r ? Object.entries(r).reduce((e, [t, n]) => e.replace(RegExp(`\\{\\{${t}\\}\\}`, "g"), String(n)), a) : a;
|
|
9
|
+
}, [n]);
|
|
10
|
+
}
|
|
11
|
+
//#endregion
|
|
12
|
+
export { n as useTr };
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=useTr.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useTr.js","names":[],"sources":["../../../../src/timecampus/shared/hooks/useTr.ts"],"sourcesContent":["import { useCallback } from \"react\";\nimport { useI18n } from \"@burdenoff/fe-libs/shared/providers/shell/I18nProvider\";\n\n/**\n * useTr — translation hook with fallback semantics.\n *\n * Wraps `useI18n().t()` so that when a key has no translation (i.e. the\n * provider returns the key itself), the caller's English fallback is used\n * instead. This keeps consumer call sites consistent and avoids leaking\n * untranslated keys into the UI.\n *\n * Supports optional interpolation params using `{{key}}` placeholders.\n *\n * @example\n * const tr = useTr();\n * return <h1>{tr('timecampus.timer.title', 'Timer')}</h1>;\n * return <p>{tr('timecampus.timer.activeCount', '{{count}} active', { count: 5 })}</p>;\n */\nexport function useTr(): (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n) => string {\n const { t } = useI18n();\n return useCallback(\n (\n key: string,\n fallback: string,\n params?: Record<string, string | number>,\n ): string => {\n const translated = t(key);\n const raw = translated === key ? fallback : translated;\n if (!params) return raw;\n return Object.entries(params).reduce(\n (str, [k, v]) =>\n str.replace(new RegExp(`\\\\{\\\\{${k}\\\\}\\\\}`, \"g\"), String(v)),\n raw,\n );\n },\n [t],\n );\n}\n"],"mappings":";;;AAkBA,SAAgB,IAIJ;CACV,IAAM,EAAE,SAAM,EAAQ;CACtB,OAAO,GAEH,GACA,GACA,MACW;EACX,IAAM,IAAa,EAAE,CAAG,GAClB,IAAM,MAAe,IAAM,IAAW;EAE5C,OADK,IACE,OAAO,QAAQ,CAAM,CAAC,CAAC,QAC3B,GAAK,CAAC,GAAG,OACR,EAAI,QAAY,OAAO,SAAS,EAAE,SAAS,GAAG,GAAG,OAAO,CAAC,CAAC,GAC5D,CACF,IALoB;CAMtB,GACA,CAAC,CAAC,CACJ;AACF"}
|
package/package.json
CHANGED