From ea9f43a725a5f08f396ed050d46f6412329d1ce0 Mon Sep 17 00:00:00 2001 From: Amr Mohammed El-Sheraey <141947355+AmrMsCLL@users.noreply.github.com> Date: Mon, 5 Oct 2026 19:02:53 +0300 Subject: [PATCH] feat: open the dashboard that fits the time of day and device --- app/frontend/src/dashboard/Dashboard.tsx | 77 +++++++++++++++- app/frontend/test/dashboard.spec.tsx | 87 ++++++++++++++++++- .../migration.sql | 3 + app/server/prisma/schema.prisma | 3 + .../src/dashboards/dashboards.service.ts | 16 +++- .../src/dashboards/dto/dashboards.dto.ts | 21 ++++- .../test/integration/dashboards.spec.ts | 21 +++++ 7 files changed, 222 insertions(+), 6 deletions(-) create mode 100644 app/server/prisma/migrations/20261005120000_dashboard_context/migration.sql diff --git a/app/frontend/src/dashboard/Dashboard.tsx b/app/frontend/src/dashboard/Dashboard.tsx index a00736e..4229a81 100644 --- a/app/frontend/src/dashboard/Dashboard.tsx +++ b/app/frontend/src/dashboard/Dashboard.tsx @@ -9,13 +9,49 @@ import { atMost, required, useFormErrors } from '../validation'; import { widgetKinds, type Widget, type WidgetSize, type WidgetType } from './widgets'; import { LoadError } from '../components/LoadError'; +type Device = 'any' | 'phone' | 'desktop'; + interface Dashboard { id: string; name: string; position: number; layout: { widgets: Widget[] }; + showFrom: number | null; + showUntil: number | null; + showOn: Device; +} + +const chosenKey = 'strata-dashboard'; + +function rememberedDashboard() { + try { + return sessionStorage.getItem(chosenKey); + } catch { + return null; + } +} + +function rememberDashboard(id: string) { + try { + sessionStorage.setItem(chosenKey, id); + } catch { + return; + } } +export function automaticDashboard>(dashboards: T[], now: Date, phone: boolean) { + const minute = now.getHours() * 60 + now.getMinutes(); + const fits = dashboards.filter( + ({ showFrom, showUntil, showOn }) => + (showFrom === null || showUntil === null || (showFrom < showUntil ? minute >= showFrom && minute < showUntil : minute >= showFrom || minute < showUntil)) && + (showOn === 'any' || showOn === (phone ? 'phone' : 'desktop')), + ); + return fits.find((dashboard) => dashboard.showFrom !== null || dashboard.showOn !== 'any') ?? fits[0] ?? dashboards[0]; +} + +const clockTime = (minutes: number | null) => (minutes === null ? '' : `${String(Math.floor(minutes / 60)).padStart(2, '0')}:${String(minutes % 60).padStart(2, '0')}`); +const minutesOf = (time: string) => (time ? Number(time.slice(0, 2)) * 60 + Number(time.slice(3, 5)) : null); + const templates = [ ['morning', 'Morning briefing', 'Clock, Today, Agenda, Renewals and spend, Inbox'], ['work', 'Work day', 'Today, Task list, Shortcuts, Clock'], @@ -134,11 +170,20 @@ export function DashboardHome() { const [name, setName] = useState(''); const [deleting, setDeleting] = useState(false); const [adding, setAdding] = useState(false); + const [schedule, setSchedule] = useState({ from: '', until: '', on: 'any' as Device }); const addForm = useFormErrors(); + const chosen = params.get('d'); + + useEffect(() => { + if (chosen) rememberDashboard(chosen); + }, [chosen]); if (dashboards.error) return ; if (!dashboards.data) return ; - const current = dashboards.data.find((dashboard) => dashboard.id === params.get('d')) ?? dashboards.data[0]!; + const picked = chosen ?? rememberedDashboard(); + const current = + dashboards.data.find((dashboard) => dashboard.id === picked) ?? + automaticDashboard(dashboards.data, new Date(), window.matchMedia('(max-width: 767px)').matches)!; const widgets = draft ?? current.layout.widgets; const editing = draft !== undefined; if (params.get('display') === 'wall' && !editing) return ; @@ -157,9 +202,17 @@ export function DashboardHome() { } void saving .run(async () => { + if (Boolean(schedule.from) !== Boolean(schedule.until)) throw new Error('Set both times, or neither.'); + const showFrom = minutesOf(schedule.from); + const showUntil = minutesOf(schedule.until); const saved = await auth.request(`/me/dashboards/${current.id}`, { method: 'PATCH', - body: JSON.stringify({ layout: { widgets }, ...(name.trim() ? { name: name.trim() } : {}) }), + body: JSON.stringify({ + layout: { widgets }, + ...(name.trim() ? { name: name.trim() } : {}), + ...(showFrom !== current.showFrom || showUntil !== current.showUntil ? { showFrom, showUntil } : {}), + ...(schedule.on !== current.showOn ? { showOn: schedule.on } : {}), + }), }); dashboards.mutate((all) => all.map((dashboard) => (dashboard.id === saved.id ? saved : dashboard))); return ''; @@ -203,6 +256,7 @@ export function DashboardHome() { className="secondary-button px-3 py-2 text-sm" onClick={() => { setName(current.name); + setSchedule({ from: clockTime(current.showFrom), until: clockTime(current.showUntil), on: current.showOn }); setDraft(current.layout.widgets); }} type="button" @@ -229,6 +283,25 @@ export function DashboardHome() { Name setName(event.currentTarget.value)} value={name} /> +
+ Opens by itself + + + +
{dashboards.data.length > 1 && (