From 0e94ad36b580534877871ece247ed6085b5749ef Mon Sep 17 00:00:00 2001
From: Mohamed Bassem <me@mbassem.com>
Date: Sun, 6 Jul 2025 18:26:41 +0000
Subject: feat: Add a new timezone user setting

---
 apps/web/components/settings/UserOptions.tsx | 80 ++++++++++++++++++++++++++--
 1 file changed, 77 insertions(+), 3 deletions(-)

(limited to 'apps/web/components/settings/UserOptions.tsx')

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"
-- 
cgit v1.2.3-70-g09d2

