diff options
| author | Mohamed Bassem <me@mbassem.com> | 2025-07-06 18:26:41 +0000 |
|---|---|---|
| committer | Mohamed Bassem <me@mbassem.com> | 2025-07-06 18:51:40 +0000 |
| commit | 0e94ad36b580534877871ece247ed6085b5749ef (patch) | |
| tree | 4585a326d2a2e36d34c6627c047ccec49b3e1b4d /apps | |
| parent | b60ece578304df21602d39c7022a7a4dbc6437e0 (diff) | |
| download | karakeep-0e94ad36b580534877871ece247ed6085b5749ef.tar.zst | |
feat: Add a new timezone user setting
Diffstat (limited to 'apps')
| -rw-r--r-- | apps/web/app/settings/stats/page.tsx | 16 | ||||
| -rw-r--r-- | apps/web/components/settings/UserOptions.tsx | 80 | ||||
| -rw-r--r-- | apps/web/lib/userSettings.tsx | 1 |
3 files changed, 94 insertions, 3 deletions
diff --git a/apps/web/app/settings/stats/page.tsx b/apps/web/app/settings/stats/page.tsx index ab4fb22c..599e5362 100644 --- a/apps/web/app/settings/stats/page.tsx +++ b/apps/web/app/settings/stats/page.tsx @@ -110,6 +110,7 @@ function StatCard({ export default function StatsPage() { const { t } = useTranslation(); const { data: stats, isLoading } = api.users.stats.useQuery(); + const { data: userSettings } = api.users.settings.useQuery(); const maxHourlyActivity = useMemo(() => { if (!stats) return 0; @@ -176,6 +177,11 @@ export default function StatsPage() { </h1> <p className="text-muted-foreground"> Insights into your bookmarking habits and collection + {userSettings?.timezone && userSettings.timezone !== "UTC" && ( + <span className="block text-sm"> + Times shown in {userSettings.timezone} timezone + </span> + )} </p> </div> @@ -443,6 +449,11 @@ export default function StatsPage() { <CardTitle className="flex items-center gap-2"> <Clock className="h-5 w-5" /> {t("settings.stats.activity_patterns.activity_by_hour")} + {userSettings?.timezone && userSettings.timezone !== "UTC" && ( + <span className="text-xs font-normal text-muted-foreground"> + ({userSettings.timezone}) + </span> + )} </CardTitle> </CardHeader> <CardContent> @@ -462,6 +473,11 @@ export default function StatsPage() { <CardTitle className="flex items-center gap-2"> <BarChart3 className="h-5 w-5" /> {t("settings.stats.activity_patterns.activity_by_day")} + {userSettings?.timezone && userSettings.timezone !== "UTC" && ( + <span className="text-xs font-normal text-muted-foreground"> + ({userSettings.timezone}) + </span> + )} </CardTitle> </CardHeader> <CardContent> diff --git a/apps/web/components/settings/UserOptions.tsx b/apps/web/components/settings/UserOptions.tsx index 4f18a2e3..02c27145 100644 --- a/apps/web/components/settings/UserOptions.tsx +++ b/apps/web/components/settings/UserOptions.tsx @@ -1,13 +1,13 @@ "use client"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useClientConfig } from "@/lib/clientConfig"; import { useTranslation } from "@/lib/i18n/client"; import { useInterfaceLang } from "@/lib/userLocalSettings/bookmarksLayout"; import { updateInterfaceLang } from "@/lib/userLocalSettings/userLocalSettings"; import { useUserSettings } from "@/lib/userSettings"; import { zodResolver } from "@hookform/resolvers/zod"; -import { Archive, Bookmark, Globe } from "lucide-react"; +import { Archive, Bookmark, Clock, Globe } from "lucide-react"; import { useForm } from "react-hook-form"; import { z } from "zod"; @@ -71,6 +71,9 @@ export default function UserOptions() { }); }, }); + const [timezones, setTimezones] = useState< + { label: string; value: string }[] | null + >(null); const bookmarkClickActionTranslation: Record< ZUserSettings["bookmarkClickAction"], @@ -92,6 +95,39 @@ export default function UserOptions() { hide: t("settings.info.user_settings.archive_display_behaviour.hide"), }; + // Get all supported timezones and format them nicely + useEffect(() => { + try { + const browserTimezones = Intl.supportedValuesOf("timeZone"); + setTimezones( + browserTimezones + .map((tz) => { + // Create a more readable label by replacing underscores with spaces + // and showing the current time offset + const now = new Date(); + const formatter = new Intl.DateTimeFormat("en", { + timeZone: tz, + timeZoneName: "short", + }); + const parts = formatter.formatToParts(now); + const timeZoneName = + parts.find((part) => part.type === "timeZoneName")?.value || ""; + + // Format the timezone name for display + const displayName = tz.replace(/_/g, " ").replace("/", " / "); + const label = timeZoneName + ? `${displayName} (${timeZoneName})` + : displayName; + + return { value: tz, label }; + }) + .sort((a, b) => a.label.localeCompare(b.label)), + ); + } catch { + setTimezones(null); + } + }, []); + const form = useForm<z.infer<typeof zUserSettingsSchema>>({ resolver: zodResolver(zUserSettingsSchema), defaultValues: data, @@ -119,9 +155,47 @@ export default function UserOptions() { <LanguageSelect /> </div> + <FormField + control={form.control} + name="timezone" + render={({ field }) => ( + <div className="space-y-2"> + <Label className="flex items-center gap-2 text-sm font-medium"> + <Clock className="h-4 w-4" /> + Timezone + </Label> + <Select + disabled={!!clientConfig.demoMode || timezones === null} + value={field.value} + onValueChange={(value) => { + mutate({ + timezone: value, + }); + }} + > + <SelectTrigger className="h-11"> + <SelectValue> + {timezones?.find( + (tz: { value: string; label: string }) => + tz.value === field.value, + )?.label || field.value} + </SelectValue> + </SelectTrigger> + <SelectContent> + {timezones?.map((tz: { value: string; label: string }) => ( + <SelectItem key={tz.value} value={tz.value}> + {tz.label} + </SelectItem> + ))} + </SelectContent> + </Select> + </div> + )} + /> + <Separator /> - <div className="grid gap-6 md:grid-cols-2"> + <div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3"> <FormField control={form.control} name="bookmarkClickAction" diff --git a/apps/web/lib/userSettings.tsx b/apps/web/lib/userSettings.tsx index 1590f727..2ab7db2b 100644 --- a/apps/web/lib/userSettings.tsx +++ b/apps/web/lib/userSettings.tsx @@ -9,6 +9,7 @@ import { api } from "./trpc"; export const UserSettingsContext = createContext<ZUserSettings>({ bookmarkClickAction: "open_original_link", archiveDisplayBehaviour: "show", + timezone: "UTC", }); export function UserSettingsContextProvider({ |
