ReviewOS

also looking at this

loghqorg/loghq

feat(analytics): presets-and-calendar reporting-period picker

#6
Merged glennmichael123 wants to merge feat/date-range-picker into main
3 files +500 -5
resources/views/analytics.stxmodified+159-5
Changes to resources/views/analytics.stx
@@ -33,6 +33,21 @@ const RANGES: AnalyticsRange[] = [
3333
3434const activeRange = RANGES.find(r => r.key === query.get('range')) ?? DEFAULT_RANGE
3535
36// Custom range: ?from=YYYY-MM-DD&to=YYYY-MM-DD, both required and valid, and it
37// wins over the preset. volumeSeries/levelsByRelease already take day bounds, so
38// a custom span needs no query changes; it is still bucketed by day like 180d.
39const qFrom = query.get('from')
40const qTo = query.get('to')
41const isDay = (v: string | null): v is string => typeof v === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(v)
42const isCustom = isDay(qFrom) && isDay(qTo)
43const customFrom = isCustom ? String(qFrom).slice(0, 10) : ''
44const customTo = isCustom ? String(qTo).slice(0, 10) : ''
45// The one-line label the picker button shows.
46const fmtDay = (d: string): string => new Date(`${d}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })
47const rangeLabel = isCustom
48 ? (customFrom.slice(0, 4) === customTo.slice(0, 4) ? `${fmtDay(customFrom)} \u2013 ${fmtDay(customTo)}, ${customTo.slice(0, 4)}` : `${fmtDay(customFrom)}, ${customFrom.slice(0, 4)} \u2013 ${fmtDay(customTo)}, ${customTo.slice(0, 4)}`)
49 : activeRange.label
50
3651const requestedProject = query.get('project')
3752const cookieProject = typeof cookies !== 'undefined' ? (cookies.loghq_project ?? '') : ''
3853
@@ -83,7 +98,7 @@ function pageUrl(overrides: Partial<{ range: string }>): string {
8398 return s ? `/analytics?${s}` : '/analytics'
8499}
85100
86const rangeTabs = RANGES.map(r => ({ ...r, href: pageUrl({ range: r.key }), active: r.key === activeRange.key }))
101const rangeTabs = RANGES.map(r => ({ ...r, href: pageUrl({ range: r.key }), active: !isCustom && r.key === activeRange.key }))
87102const switchUrl = pageUrl({})
88103
89104const cfg = archiveConfig()
@@ -114,10 +129,18 @@ async function loadData(): Promise<void> {
114129 return
115130
116131 const now = new Date()
117 const from = new Date(now.getTime())
118 from.setUTCDate(from.getUTCDate() - activeRange.days)
119 const fromDay = dayOfUtc(from)
120 const toDay = dayOfUtc(now)
132 let fromDay: string
133 let toDay: string
134 if (isCustom) {
135 fromDay = customFrom
136 toDay = customTo
137 }
138 else {
139 const from = new Date(now.getTime())
140 from.setUTCDate(from.getUTCDate() - activeRange.days)
141 fromDay = dayOfUtc(from)
142 toDay = dayOfUtc(now)
143 }
121144
122145 plan = await projectPlan(projectId)
123146 series = await volumeSeries(projectId, { fromDay, toDay, unit: 'day' })
@@ -216,6 +239,136 @@ onMounted(() => {
216239
217240@section('content')
218241
242
243{{-- Date-range picker styling. A loose <style> under a layout: stx sweeps it
244 into the page (loghq's default layout has no @stack('styles')). Self-contained
245 to the picker, themed on the app's tokens. --}}
246<style>
247.range-tool {
248 display: inline-flex;
249 align-items: center;
250 justify-content: center;
251 gap: 0.4rem;
252 min-height: 2.25rem;
253 padding: 0 0.72rem;
254 border: 1px solid var(--border);
255 border-radius: 9px;
256 background: var(--panel);
257 color: var(--text-2);
258 font: 600 0.8125rem/1 var(--sans, ui-sans-serif);
259 text-decoration: none;
260 cursor: pointer;
261 transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease, transform 0.15s ease;
262}
263.range-tool:hover { color: var(--text); border-color: var(--accent); }
264.range-tool:active { transform: scale(0.98); }
265.range-tool:focus-visible,
266.range-link:focus-visible,
267.chrome-btn:focus-visible,
268.icon-btn:focus-visible,
269select.panel:focus-visible,
270input:focus-visible,
271textarea:focus-visible,
272summary:focus-visible {
273 outline: 2px solid var(--accent);
274 outline-offset: 2px;
275}
276.range-tool.is-active { color: var(--accent-ink); background: var(--accent); border-color: var(--accent); }
277.range-tool i { width: 1rem; height: 1rem; }
278.date-menu { position: relative; }
279.date-menu > summary { list-style: none; }
280.date-menu > summary::-webkit-details-marker { display: none; }
281.date-popover {
282 position: absolute;
283 right: 0;
284 top: calc(100% + 0.5rem);
285 z-index: 40;
286 width: min(44rem, calc(100vw - 2.5rem));
287 padding: 0.75rem;
288 border: 1px solid var(--border);
289 border-radius: 12px;
290 background: var(--panel);
291 box-shadow: 0 18px 50px color-mix(in srgb, var(--text) 10%, transparent);
292}
293/* Period picker: presets down the left, two months on the right. The range is
294 drawn flat with rounded ends, the way Fathom's is, on the --bar wash the KPI
295 bars already use so it reads as the same accent at the same strength. */
296.drp-backdrop { position: fixed; inset: 0; z-index: 39; background: transparent; cursor: default; }
297.drp { display: grid; grid-template-columns: 10.5rem minmax(0, 1fr); gap: 0.75rem; }
298.drp-presets { display: flex; flex-direction: column; gap: 0.15rem; padding-right: 0.75rem; border-right: 1px solid var(--border); }
299.drp-preset {
300 display: block;
301 width: 100%;
302 padding: 0.5rem 0.7rem;
303 border: 0;
304 border-radius: 8px;
305 background: transparent;
306 color: var(--text-2);
307 font: 500 0.8125rem/1.2 var(--sans, ui-sans-serif);
308 text-align: left;
309 cursor: pointer;
310}
311.drp-preset:hover { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }
312.drp-preset.is-active { color: var(--accent-ink); background: var(--accent); }
313.drp-cals { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
314.drp-head { display: grid; grid-template-columns: 2rem minmax(0, 1fr) minmax(0, 1fr) 2rem; align-items: center; gap: 0.35rem; margin-bottom: 0.5rem; }
315.drp-nav {
316 display: inline-grid;
317 place-items: center;
318 width: 2rem;
319 height: 2rem;
320 padding: 0;
321 border: 1px solid transparent;
322 border-radius: 8px;
323 background: transparent;
324 color: var(--text-2);
325 cursor: pointer;
326}
327.drp-nav:hover { color: var(--text); border-color: var(--border); }
328.drp-nav svg { width: 1rem; height: 1rem; }
329.drp-nav.is-hidden { visibility: hidden; }
330.drp-sel {
331 min-width: 0;
332 height: 2rem;
333 padding: 0 0.5rem;
334 border: 1px solid var(--border);
335 border-radius: 8px;
336 background: var(--bg);
337 color: var(--text);
338 font: 600 0.8125rem/1 var(--sans, ui-sans-serif);
339}
340.drp-dow, .drp-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
341.drp-dow span { padding: 0.25rem 0; color: var(--text-3); font: 600 0.7rem/1.2 var(--sans, ui-sans-serif); text-align: center; }
342.drp-day {
343 height: 2.25rem;
344 padding: 0;
345 border: 0;
346 border-radius: 0;
347 background: transparent;
348 color: var(--text);
349 font: 500 0.8125rem/1 var(--mono, ui-monospace);
350 cursor: pointer;
351}
352.drp-day:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
353.drp-day.is-out { color: var(--text-3); }
354.drp-day.is-today { font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
355.drp-day.is-range { background: var(--accent-soft); }
356.drp-day.is-edge { background: var(--accent); color: var(--accent-ink); border-radius: 8px; }
357.drp-day.is-disabled { color: var(--text-3); opacity: 0.45; cursor: default; }
358.drp-day.is-disabled:hover { background: transparent; }
359.drp-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
360@media (max-width: 1023px) {
361 .date-menu { position: static; }
362 .date-popover { left: 0; right: auto; width: calc(100vw - 2rem); max-height: calc(100dvh - 6rem); overflow-y: auto; overscroll-behavior: contain; }
363 .drp { grid-template-columns: minmax(0, 1fr); }
364 .drp-presets { flex-direction: row; flex-wrap: wrap; gap: 0.35rem; padding: 0 0 0.6rem; border-right: 0; border-bottom: 1px solid var(--border); }
365 .drp-preset { width: auto; padding: 0.45rem 0.65rem; border: 1px solid var(--border); border-radius: 999px; }
366 .drp-cals { grid-template-columns: minmax(0, 1fr); }
367 .drp-cal.is-right { display: none; }
368 .drp-cal.is-left .drp-nav.is-hidden { visibility: visible; }
369 .drp-day { height: 2.5rem; }
370}
371</style>
219372<div class="grain" aria-hidden="true"></div>
220373
221374<div class="px-5 py-10 md:px-10 min-h-[100dvh] page-dashboard">
@@ -268,6 +421,7 @@ onMounted(() => {
268421 </span>
269422 </div>
270423 <nav class="flex flex-wrap gap-1.5 items-center">
424 <DateRangePicker :summary="rangeLabel" :active="isCustom" />
271425 @foreach (rangeTabs as r)
272426 <StxLink to="" class="px-3 py-1.5 font-medium text-sm border pill ">{{ r.label }}</StxLink>
273427 @endforeach