Openstatus www.openstatus.dev

feat: add preferred-settings cookie to save user preferences (#707)

authored by

Maximilian Kaske and committed by
GitHub
c961ee3c ac553a23

+89 -3
+14 -3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/_components/combined-chart-wrapper.tsx
··· 1 1 "use client"; 2 2 3 - import { useMemo, useState } from "react"; 3 + import { useMemo } from "react"; 4 4 import { LineChart } from "lucide-react"; 5 5 6 6 import type { Monitor } from "@openstatus/db/src/schema"; ··· 11 11 } from "@openstatus/tinybird"; 12 12 import { Toggle } from "@openstatus/ui"; 13 13 14 + import { usePreferredSettings } from "@/lib/preferred-settings/client"; 15 + import type { PreferredSettings } from "@/lib/preferred-settings/server"; 14 16 import { IntervalPreset } from "../../_components/interval-preset"; 15 17 import { QuantilePreset } from "../../_components/quantile-preset"; 16 18 import { RegionsPreset } from "../../_components/region-preset"; ··· 28 30 monitor, 29 31 isQuantileDisabled, 30 32 metricsByRegion, 33 + preferredSettings: defaultPreferredSettings, 31 34 }: { 32 35 data: ResponseGraph[]; 33 36 period: Period; ··· 37 40 monitor: Monitor; 38 41 isQuantileDisabled: boolean; 39 42 metricsByRegion: ResponseTimeMetricsByRegion[]; 43 + preferredSettings: PreferredSettings; 40 44 }) { 41 45 const chartData = useMemo( 42 46 () => groupDataByTimestamp(data, period, quantile), 43 47 [data, period, quantile], 44 48 ); 45 - const [combinedRegions, setCombinedRegions] = useState(false); 49 + 50 + const [preferredSettings, setPreferredSettings] = usePreferredSettings( 51 + defaultPreferredSettings, 52 + ); 53 + 54 + const combinedRegions = preferredSettings?.combinedRegions ?? false; 46 55 47 56 return ( 48 57 <> ··· 52 61 <p className="text-muted-foreground text-xs">Change the view</p> 53 62 <Toggle 54 63 pressed={combinedRegions} 55 - onPressedChange={setCombinedRegions} 64 + onPressedChange={(value) => { 65 + setPreferredSettings({ combinedRegions: value }); 66 + }} 56 67 variant="outline" 57 68 className="w-max" 58 69 >
+3
apps/web/src/app/app/[workspaceSlug]/(dashboard)/monitors/[id]/overview/page.tsx
··· 8 8 import { Separator } from "@openstatus/ui"; 9 9 10 10 import { env } from "@/env"; 11 + import { getPreferredSettings } from "@/lib/preferred-settings/server"; 11 12 import { api } from "@/trpc/server"; 12 13 import { ButtonReset } from "../_components/button-reset"; 13 14 import { DatePickerPreset } from "../_components/date-picker-preset"; ··· 51 52 }) { 52 53 const id = params.id; 53 54 const search = searchParamsSchema.safeParse(searchParams); 55 + const preferredSettings = getPreferredSettings(); 54 56 55 57 const monitor = await api.monitor.getMonitorById.query({ 56 58 id: Number(id), ··· 110 112 monitor={monitor} 111 113 isQuantileDisabled={isQuantileDisabled} 112 114 metricsByRegion={metricsByRegion} 115 + preferredSettings={preferredSettings} 113 116 /> 114 117 </div> 115 118 );
+46
apps/web/src/lib/preferred-settings/client.ts
··· 1 + import { useState } from "react"; 2 + 3 + import { COOKIE_NAME } from "./shared"; 4 + import type { PreferredSettings } from "./validation"; 5 + import { preferencesSchema } from "./validation"; 6 + 7 + function getPreferredSettingsCookie() { 8 + const cookie = document.cookie 9 + .split(";") 10 + .find((cookie) => cookie.trim().startsWith(`${COOKIE_NAME}=`)); 11 + if (!cookie) return {}; 12 + const settings = preferencesSchema.safeParse( 13 + JSON.parse(cookie.split("=")[1]), 14 + ); 15 + if (!settings.success) return {}; 16 + return settings.data; 17 + } 18 + 19 + function setPreferredSettingsCookie(value: Record<string, unknown>) { 20 + const month = 30 * 24 * 60 * 60 * 1000; 21 + const expires = new Date(Date.now() + month).toUTCString(); 22 + document.cookie = `${COOKIE_NAME}=${JSON.stringify( 23 + value, 24 + )}; path=/; expires=${expires}`; 25 + } 26 + 27 + /** 28 + * Update user preferences and store them in a cookie accessible on the client and server 29 + */ 30 + export function usePreferredSettings(defaultValue: PreferredSettings) { 31 + const [settings, setSettings] = useState<PreferredSettings>(defaultValue); 32 + 33 + const handleChange = (value: Record<string, unknown>) => { 34 + const settings = preferencesSchema.safeParse(value); 35 + 36 + if (!settings.success) return; 37 + 38 + const currentSettings = getPreferredSettingsCookie(); 39 + const newSettings = { ...currentSettings, ...settings.data }; 40 + 41 + setPreferredSettingsCookie(newSettings); 42 + setSettings(newSettings); 43 + }; 44 + 45 + return [settings, handleChange] as const; 46 + }
+14
apps/web/src/lib/preferred-settings/server.ts
··· 1 + import { cookies } from "next/headers"; 2 + 3 + import { COOKIE_NAME } from "./shared"; 4 + import { preferencesSchema } from "./validation"; 5 + 6 + export function getPreferredSettings() { 7 + const cookie = cookies().get(COOKIE_NAME); 8 + const parsed = cookie ? JSON.parse(cookie.value) : {}; 9 + const settings = preferencesSchema.safeParse(parsed); 10 + if (!settings.success) return undefined; 11 + return settings.data; 12 + } 13 + 14 + export type PreferredSettings = ReturnType<typeof getPreferredSettings>;
+1
apps/web/src/lib/preferred-settings/shared.ts
··· 1 + export const COOKIE_NAME = "preferred-settings";
+11
apps/web/src/lib/preferred-settings/validation.ts
··· 1 + import { z } from "zod"; 2 + 3 + export const preferencesSchema = z 4 + .object({ 5 + combinedRegions: z.boolean().nullable().default(false).optional(), 6 + // ... other settings to store user preferences 7 + // accessible via document.cookie in the client and cookies() on the server 8 + }) 9 + .optional(); 10 + 11 + export type PreferredSettings = z.infer<typeof preferencesSchema>;