ReviewOS

also looking at this

stacks/analyticshq

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

#55
Merged glennmichael123 wants to merge feat/date-range-picker into main
3 files +457 -34
resources/views/dashboard.stxmodified+77-34
Changes to resources/views/dashboard.stx
@@ -54,6 +54,15 @@ let from = isAll
5454 : isCustom ? `${customFrom}T00:00:00.000Z` : new Date(Date.now() - days * 864e5).toISOString()
5555if (isCustom)
5656 days = Math.max(1, Math.round((new Date(to).getTime() - new Date(from).getTime()) / 864e5))
57// What the period control reads. A preset keeps its name and the days it resolves
58// to; 'all' has no start until loadData finds the earliest pageview, so it is
59// named alone. Dates are formatted in UTC because from/to are UTC day bounds.
60const RANGE_LABELS = { '24h': 'Last 24 hours', '7d': 'Last 7 days', '30d': 'Last 30 days', '90d': 'Last 90 days', '1y': 'Last 365 days', 'all': 'All time' }
61const fmtDay = s => new Date(`${s.slice(0, 10)}T00:00:00Z`).toLocaleDateString('en-US', { month: 'short', day: 'numeric', timeZone: 'UTC' })
62const fmtSpan = (a, b) => a.slice(0, 4) === b.slice(0, 4)
63 ? `${fmtDay(a)} – ${fmtDay(b)}, ${b.slice(0, 4)}`
64 : `${fmtDay(a)}, ${a.slice(0, 4)} – ${fmtDay(b)}, ${b.slice(0, 4)}`
65const rangeLabel = isCustom ? fmtSpan(customFrom, customTo) : isAll ? 'All time' : `${RANGE_LABELS[range] ?? 'Custom'} · ${fmtSpan(from, to)}`
5766// Chart bucket granularity by span: hour (<= 2d) for a real 24h curve, day (<= ~400d),
5867// or month (wider, e.g. all-time) so a multi-year chart isn't thousands of points.
5968// bucketLen = the LEFT(timestamp, N) prefix — 'YYYY-MM-DDTHH'(13) / 'YYYY-MM-DD'(10) /
@@ -2169,30 +2178,79 @@ summary:focus-visible {
21692178 right: 0;
21702179 top: calc(100% + 0.5rem);
21712180 z-index: 40;
2172 width: min(24rem, calc(100vw - 2.5rem));
2173 padding: 1rem;
2181 width: min(44rem, calc(100vw - 2.5rem));
2182 padding: 0.75rem;
21742183 border: 1px solid var(--border);
21752184 border-radius: 12px;
21762185 background: var(--panel);
21772186 box-shadow: 0 18px 50px color-mix(in srgb, var(--text) 10%, transparent);
21782187}
2179.date-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; }
2180.date-field label {
2188/* Period picker: presets down the left, two months on the right. The range is
2189 drawn flat with rounded ends, the way Fathom's is, on the --bar wash the KPI
2190 bars already use so it reads as the same accent at the same strength. */
2191.drp { display: grid; grid-template-columns: 10.5rem minmax(0, 1fr); gap: 0.75rem; }
2192.drp-presets { display: flex; flex-direction: column; gap: 0.15rem; padding-right: 0.75rem; border-right: 1px solid var(--border); }
2193.drp-preset {
21812194 display: block;
2182 margin-bottom: 0.35rem;
2195 width: 100%;
2196 padding: 0.5rem 0.7rem;
2197 border: 0;
2198 border-radius: 8px;
2199 background: transparent;
2200 color: var(--text-2);
2201 font: 500 0.8125rem/1.2 var(--sans, ui-sans-serif);
2202 text-align: left;
2203 cursor: pointer;
2204}
2205.drp-preset:hover { color: var(--text); background: color-mix(in srgb, var(--text) 6%, transparent); }
2206.drp-preset.is-active { color: var(--accent-ink); background: var(--accent); }
2207.drp-cals { display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; }
2208.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; }
2209.drp-nav {
2210 display: inline-grid;
2211 place-items: center;
2212 width: 2rem;
2213 height: 2rem;
2214 padding: 0;
2215 border: 1px solid transparent;
2216 border-radius: 8px;
2217 background: transparent;
21832218 color: var(--text-2);
2184 font: 600 0.75rem/1.2 var(--sans, ui-sans-serif);
2219 cursor: pointer;
21852220}
2186.date-field input {
2187 width: 100%;
2188 padding: 0.55rem 0.65rem;
2221.drp-nav:hover { color: var(--text); border-color: var(--border); }
2222.drp-nav svg { width: 1rem; height: 1rem; }
2223.drp-nav.is-hidden { visibility: hidden; }
2224.drp-sel {
2225 min-width: 0;
2226 height: 2rem;
2227 padding: 0 0.5rem;
21892228 border: 1px solid var(--border);
21902229 border-radius: 8px;
21912230 background: var(--bg);
21922231 color: var(--text);
2232 font: 600 0.8125rem/1 var(--sans, ui-sans-serif);
2233}
2234.drp-dow, .drp-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); }
2235.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; }
2236.drp-day {
2237 height: 2.25rem;
2238 padding: 0;
2239 border: 0;
2240 border-radius: 0;
2241 background: transparent;
2242 color: var(--text);
21932243 font: 500 0.8125rem/1 var(--mono, ui-monospace);
2244 cursor: pointer;
21942245}
2195.date-submit { width: 100%; margin-top: 0.75rem; }
2246.drp-day:hover { background: color-mix(in srgb, var(--accent) 22%, transparent); }
2247.drp-day.is-out { color: var(--text-3); }
2248.drp-day.is-today { font-weight: 700; text-decoration: underline; text-underline-offset: 3px; }
2249.drp-day.is-range { background: var(--bar); }
2250.drp-day.is-edge { background: var(--accent); color: var(--accent-ink); border-radius: 8px; }
2251.drp-day.is-disabled { color: var(--text-3); opacity: 0.45; cursor: default; }
2252.drp-day.is-disabled:hover { background: transparent; }
2253.drp-day:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; border-radius: 8px; }
21962254.live-status {
21972255 display: inline-flex;
21982256 align-items: center;
@@ -2242,7 +2300,14 @@ summary:focus-visible {
22422300 .range-link { flex: 1 0 auto; }
22432301 .range-actions { justify-content: flex-start; }
22442302 .date-menu { position: static; }
2245 .date-popover { left: 0; right: auto; }
2303 .date-popover { left: 0; right: auto; width: calc(100vw - 2rem); max-height: calc(100dvh - 6rem); overflow-y: auto; overscroll-behavior: contain; }
2304 .drp { grid-template-columns: minmax(0, 1fr); }
2305 .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); }
2306 .drp-preset { width: auto; padding: 0.45rem 0.65rem; border: 1px solid var(--border); border-radius: 999px; }
2307 .drp-cals { grid-template-columns: minmax(0, 1fr); }
2308 .drp-cal.is-right { display: none; }
2309 .drp-cal.is-left .drp-nav.is-hidden { visibility: visible; }
2310 .drp-day { height: 2.5rem; }
22462311}
22472312
22482313/* Site switcher. Surface, border and radius come from .panel. The native menu remains
@@ -2434,29 +2499,7 @@ select.panel:hover { border-color: var(--accent); }
24342499 </nav>
24352500
24362501 <div class="range-actions">
2437 <details class="date-menu">
2438 <summary class="range-tool ">
2439 <i class="i-hugeicons-calendar-03" aria-hidden="true"></i>
2440 {{ isCustom ? (customFrom + ' to ' + customTo) : 'Custom' }}
2441 </summary>
2442 <form method="get" class="date-popover" aria-label="Custom date range">
2443 <input type="hidden" name="site" value="">
2444 @if (compare)
2445 <input type="hidden" name="compare" value="1">
2446 @endif
2447 <div class="date-grid">
2448 <div class="date-field">
2449 <label for="range-from">From</label>
2450 <input id="range-from" type="date" name="from" value="">
2451 </div>
2452 <div class="date-field">
2453 <label for="range-to">To</label>
2454 <input id="range-to" type="date" name="to" value="">
2455 </div>
2456 </div>
2457 <button type="submit" class="date-submit is-active range-tool">Apply range</button>
2458 </form>
2459 </details>
2502 <DateRangePicker :summary="rangeLabel" :active="isCustom" />
24602503 <StxLink
24612504 to=""
24622505 title="Overlay the previous period on the chart"