diff --git a/apps/web/src/api/server-functions/getAppLayout.ts b/apps/web/src/api/server-functions/getAppLayout.ts index 8153caa10..5e37173f3 100644 --- a/apps/web/src/api/server-functions/getAppLayout.ts +++ b/apps/web/src/api/server-functions/getAppLayout.ts @@ -1,20 +1,10 @@ import { createServerFn } from '@tanstack/react-start'; import { getCookie } from '@tanstack/react-start/server'; import Cookies from 'js-cookie'; -import { z } from 'zod'; import type { LayoutSize } from '@/components/ui/layouts/AppLayout'; import { createAutoSaveId } from '@/components/ui/layouts/AppSplitter/create-auto-save-id'; import { isServer } from '@/lib/window'; - -const getServerCookie = createServerFn({ method: 'GET' }) - .validator( - z.object({ - cookieName: z.string(), - }) - ) - .handler(async ({ data }) => { - return getCookie(data.cookieName); - }); +import { getServerCookie } from './getServerCookie'; export const getAppLayout = async ({ id, diff --git a/apps/web/src/api/server-functions/getServerCookie.ts b/apps/web/src/api/server-functions/getServerCookie.ts new file mode 100644 index 000000000..f3dccc465 --- /dev/null +++ b/apps/web/src/api/server-functions/getServerCookie.ts @@ -0,0 +1,13 @@ +import { createServerFn } from '@tanstack/react-start'; +import { getCookie } from '@tanstack/react-start/server'; +import { z } from 'zod'; + +export const getServerCookie = createServerFn({ method: 'GET' }) + .validator( + z.object({ + cookieName: z.string(), + }) + ) + .handler(async ({ data }) => { + return getCookie(data.cookieName); + }); diff --git a/apps/web/src/controllers/HomePage/HomePageController.tsx b/apps/web/src/controllers/HomePage/HomePageController.tsx index 35dda7201..76b8ef1e7 100644 --- a/apps/web/src/controllers/HomePage/HomePageController.tsx +++ b/apps/web/src/controllers/HomePage/HomePageController.tsx @@ -1,8 +1,10 @@ import type React from 'react'; -import { useMemo } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useGetUserBasicInfo } from '@/api/buster_rest/users/useGetUserInfo'; import { Title } from '@/components/ui/typography'; +import { useAsyncEffect } from '@/hooks/useAsyncEffect'; import { cn } from '@/lib/classMerge'; +import { getCurrentTimeInClientTimezone, initializeClientTimezone } from '@/lib/timezone'; import { NewChatInput } from './NewChatInput'; import { NewChatWarning } from './NewChatWarning'; import { useNewChatWarning } from './useNewChatWarning'; @@ -49,19 +51,30 @@ export const HomePageController: React.FC<{ const useGreeting = () => { const user = useGetUserBasicInfo(); const userName = user?.name; + const [timeOfDay, setTimeOfDay] = useState(null); - const timeOfDay = useMemo(() => { - const now = new Date(); - const hours = now.getHours(); + // Initialize timezone detection on client mount + useEffect(() => { + initializeClientTimezone(); + }, []); + + useAsyncEffect(async () => { + const now = await getCurrentTimeInClientTimezone(); + const hours = now.hour(); + + let calculatedTimeOfDay: TimeOfDay; if (hours >= 5 && hours < 12) { - return TimeOfDay.MORNING; - } else if (hours >= 12 && hours < 17) { - return TimeOfDay.AFTERNOON; - } else if (hours >= 17 && hours < 21) { - return TimeOfDay.EVENING; + calculatedTimeOfDay = TimeOfDay.MORNING; + } else if (hours >= 12 && hours < 18) { + calculatedTimeOfDay = TimeOfDay.AFTERNOON; + } else if (hours >= 18 && hours < 22) { + calculatedTimeOfDay = TimeOfDay.EVENING; } else { - return TimeOfDay.NIGHT; + calculatedTimeOfDay = TimeOfDay.NIGHT; } + + setTimeOfDay(calculatedTimeOfDay); + return undefined; }, []); const greeting = useMemo(() => { diff --git a/apps/web/src/lib/timezone.ts b/apps/web/src/lib/timezone.ts new file mode 100644 index 000000000..6b17313a4 --- /dev/null +++ b/apps/web/src/lib/timezone.ts @@ -0,0 +1,80 @@ +import dayjs from 'dayjs'; +import timezone from 'dayjs/plugin/timezone'; +import utc from 'dayjs/plugin/utc'; +import cookies from 'js-cookie'; +import { getServerCookie } from '@/api/server-functions/getServerCookie'; +import { isServer } from './window'; + +dayjs.extend(utc); +dayjs.extend(timezone); + +const TIMEZONE_COOKIE_KEY = 'client-timezone'; +const TIMEZONE_COOKIE_EXPIRY = 30; // 30 days + +/** + * Get the client's timezone from browser API + */ +export const detectClientTimezone = (): string => { + if (isServer) return ''; + + try { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } catch { + return ''; + } +}; + +/** + * Store the client's timezone in a cookie + */ +export const setTimezoneInCookie = (timezone: string): void => { + if (isServer || !timezone) return; + + cookies.set(TIMEZONE_COOKIE_KEY, timezone, { + expires: TIMEZONE_COOKIE_EXPIRY, + sameSite: 'lax', + secure: true, + }); +}; + +/** + * Get timezone from cookie (works on both server and client) + */ +export const getTimezoneFromCookie = async (): Promise => { + if (isServer) { + const cookieData = await getServerCookie({ data: { cookieName: TIMEZONE_COOKIE_KEY } }); + return cookieData || null; + } + + return cookies.get(TIMEZONE_COOKIE_KEY) || null; +}; + +/** + * Initialize client timezone detection and storage + * Call this once on client mount + */ +export const initializeClientTimezone = async () => { + if (isServer) return; + + const storedTimezone = await getTimezoneFromCookie(); + const currentTimezone = detectClientTimezone(); + + // Only update cookie if timezone has changed or is missing + if (!storedTimezone || storedTimezone !== currentTimezone) { + setTimezoneInCookie(currentTimezone); + } +}; + +/** + * Get current time in the client's timezone (or fallback to local time) + */ +export const getCurrentTimeInClientTimezone = async (): Promise => { + const clientTimezone = await getTimezoneFromCookie(); + + if (clientTimezone) { + return dayjs().tz(clientTimezone); + } + + // Fallback to local time if no timezone available + return dayjs(); +}; diff --git a/package.json b/package.json index b7ae1d1cd..6ec8b0018 100644 --- a/package.json +++ b/package.json @@ -64,7 +64,7 @@ "peerDependencies": { "typescript": "^5" }, - "packageManager": "pnpm@10.15.0", + "packageManager": "pnpm@10.15.1", "pnpm": { "peerDependencyRules": { "ignoreMissing": [