From a74acfcfaa21b36e4d0f9cfe9e179f21e1512e85 Mon Sep 17 00:00:00 2001 From: Riyadh Date: Tue, 21 Apr 2026 07:16:50 +0000 Subject: [PATCH] Task #28: Let users pick a 12-hour (AM/PM) clock instead of 24-hour MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a per-user 12/24-hour clock preference that complements the existing clock-style preference and is honored by every clock variant on the home screen. Schema & API - Added `clockHour12 boolean` (nullable) column to `users` table (lib/db/src/schema/users.ts) and synced via direct `ALTER TABLE` because `drizzle-kit push` was blocked by an unrelated interactive rename prompt for the existing `app_opens` table. - Extended OpenAPI `AuthUser` and `UserProfile` with `clockHour12`, added `UpdateClockHour12Body` schema, and a new `PATCH /auth/me/clock-hour12` endpoint. Regenerated zod + react-query clients via `pnpm --filter @workspace/api-spec run codegen`. - Implemented the new route handler in `artifacts/api-server/src/routes/auth.ts`; `buildAuthUser` now surfaces `clockHour12`. Frontend - `lib/i18n-format.ts` no longer hard-codes `hour12: false`; callers may pass `hour12` in options. Default remains 24-hour to keep all other timestamps unchanged (chat etc. left as-is — see follow-up). - `components/clock.tsx` exports `resolveClockHour12` and threads a new `hour12` prop through `Clock` and `AnalogClockWidget`. All five variants (full/digital/digital-no-seconds/analog/minimal) plus the large analog widget now honor the choice. - `components/clock-style-picker.tsx` gained a 12-hour / 24-hour segmented toggle that calls the new endpoint with optimistic cache updates. The variant previews also reflect the active hour format. - `pages/home.tsx` passes `user.clockHour12` to the header clock, widget, and picker. - Added `home.clockStyle.hourFormat.{label,h12,h24}` strings in EN and AR. Arabic uses Latin digits and "ص/م" via Intl's localized dayPeriod. Verification - `pnpm -w run typecheck` passes. - E2E test: logged in, switched to 12-hour, verified AM/PM in header and previews, reloaded to confirm persistence, switched back to 24-hour, reloaded again — all green. --- artifacts/api-server/src/routes/auth.ts | 24 +++++ artifacts/teaboy-os/public/opengraph.jpg | Bin 25900 -> 25867 bytes .../src/components/clock-style-picker.tsx | 71 +++++++++++++- artifacts/teaboy-os/src/components/clock.tsx | 21 ++++- artifacts/teaboy-os/src/lib/i18n-format.ts | 3 +- artifacts/teaboy-os/src/locales/ar.json | 5 + artifacts/teaboy-os/src/locales/en.json | 5 + artifacts/teaboy-os/src/pages/home.tsx | 18 +++- .../src/generated/api.schemas.ts | 12 +++ lib/api-client-react/src/generated/api.ts | 87 ++++++++++++++++++ lib/api-spec/openapi.yaml | 38 ++++++++ lib/api-zod/src/generated/api.ts | 41 +++++++++ lib/db/src/schema/users.ts | 1 + 13 files changed, 318 insertions(+), 8 deletions(-) diff --git a/artifacts/api-server/src/routes/auth.ts b/artifacts/api-server/src/routes/auth.ts index 312a184e..712013cf 100644 --- a/artifacts/api-server/src/routes/auth.ts +++ b/artifacts/api-server/src/routes/auth.ts @@ -16,6 +16,7 @@ import { LoginBody, UpdateLanguageBody, UpdateClockStyleBody, + UpdateClockHour12Body, ForgotPasswordBody, ResetPasswordBody, VerifyResetTokenBody, @@ -38,6 +39,7 @@ function buildAuthUser(user: typeof usersTable.$inferSelect, roles: string[]) { displayNameEn: user.displayNameEn, preferredLanguage: user.preferredLanguage, clockStyle: user.clockStyle, + clockHour12: user.clockHour12, avatarUrl: user.avatarUrl, isActive: user.isActive, roles, @@ -326,6 +328,28 @@ router.patch("/auth/me/language", requireAuth, async (req, res): Promise = res.json(buildAuthUser(user, roles)); }); +router.patch("/auth/me/clock-hour12", requireAuth, async (req, res): Promise => { + const parsed = UpdateClockHour12Body.safeParse(req.body); + if (!parsed.success) { + res.status(400).json({ error: parsed.error.message }); + return; + } + + const [user] = await db + .update(usersTable) + .set({ clockHour12: parsed.data.clockHour12 ?? null }) + .where(eq(usersTable.id, req.session.userId!)) + .returning(); + + if (!user) { + res.status(401).json({ error: "User not found" }); + return; + } + + const roles = await getUserRoles(user.id); + res.json(buildAuthUser(user, roles)); +}); + router.patch("/auth/me/clock-style", requireAuth, async (req, res): Promise => { const parsed = UpdateClockStyleBody.safeParse(req.body); if (!parsed.success) { diff --git a/artifacts/teaboy-os/public/opengraph.jpg b/artifacts/teaboy-os/public/opengraph.jpg index 6e7ad37468c83e4f7b5de926df4b33716a58ca2f..8c2ac7c47030b29fdad680e116b78f18323d4fe9 100644 GIT binary patch literal 25867 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 diff --git a/artifacts/teaboy-os/src/components/clock-style-picker.tsx b/artifacts/teaboy-os/src/components/clock-style-picker.tsx index de86502c..c16514db 100644 --- a/artifacts/teaboy-os/src/components/clock-style-picker.tsx +++ b/artifacts/teaboy-os/src/components/clock-style-picker.tsx @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; import { useQueryClient } from "@tanstack/react-query"; import { useUpdateClockStyle, + useUpdateClockHour12, getGetMeQueryKey, type AuthUser, type ClockStyle as ClockStyleType, @@ -17,15 +18,17 @@ import { Clock, CLOCK_STYLES, resolveClockStyle, + resolveClockHour12, useNow, useHomeClockVisibility, } from "@/components/clock"; type Props = { currentStyle: ClockStyleType | null | undefined; + currentHour12: boolean | null | undefined; }; -export function ClockStylePicker({ currentStyle }: Props) { +export function ClockStylePicker({ currentStyle, currentHour12 }: Props) { const { t, i18n: i18nInstance } = useTranslation(); const lang = i18nInstance.language; const queryClient = useQueryClient(); @@ -33,7 +36,9 @@ export function ClockStylePicker({ currentStyle }: Props) { const [open, setOpen] = useState(false); const updateClockStyle = useUpdateClockStyle(); + const updateClockHour12 = useUpdateClockHour12(); const active = resolveClockStyle(currentStyle); + const activeHour12 = resolveClockHour12(currentHour12); const [homeClockVisible, setHomeClockVisible] = useHomeClockVisibility(); const choose = (style: ClockStyleType) => { @@ -62,6 +67,32 @@ export function ClockStylePicker({ currentStyle }: Props) { setOpen(false); }; + const chooseHour12 = (next: boolean) => { + if (next === activeHour12) return; + const previous = queryClient.getQueryData(getGetMeQueryKey()); + if (previous) { + queryClient.setQueryData(getGetMeQueryKey(), { + ...previous, + clockHour12: next, + }); + } + updateClockHour12.mutate( + { data: { clockHour12: next } }, + { + onSuccess: (data) => { + queryClient.setQueryData(getGetMeQueryKey(), data); + }, + onError: () => { + if (previous) { + queryClient.setQueryData(getGetMeQueryKey(), previous); + } else { + queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }); + } + }, + }, + ); + }; + return ( @@ -105,6 +136,36 @@ export function ClockStylePicker({ currentStyle }: Props) {
+
+ {t("home.clockStyle.hourFormat.label")} +
+
+ {([false, true] as const).map((value) => { + const isActive = value === activeHour12; + const labelKey = value ? "h12" : "h24"; + return ( + + ); + })} +
+
{t("home.clockStyle.label")}
@@ -123,7 +184,13 @@ export function ClockStylePicker({ currentStyle }: Props) { >
- +
{t(`home.clockStyle.options.${style}`)} diff --git a/artifacts/teaboy-os/src/components/clock.tsx b/artifacts/teaboy-os/src/components/clock.tsx index bfe1d200..7050850e 100644 --- a/artifacts/teaboy-os/src/components/clock.tsx +++ b/artifacts/teaboy-os/src/components/clock.tsx @@ -36,9 +36,16 @@ type ClockProps = { style: ClockStyle | null | undefined; time: Date; lang: string; + hour12?: boolean | null; size?: "sm" | "md"; }; +export const DEFAULT_CLOCK_HOUR12 = false; + +export function resolveClockHour12(value: boolean | null | undefined): boolean { + return value ?? DEFAULT_CLOCK_HOUR12; +} + function AnalogClock({ time, size = "md" }: { time: Date; size?: "sm" | "md" }) { const hours = time.getHours() % 12; const minutes = time.getMinutes(); @@ -128,12 +135,15 @@ function AnalogClock({ time, size = "md" }: { time: Date; size?: "sm" | "md" }) export function AnalogClockWidget({ time, lang, + hour12, className, }: { time: Date; lang: string; + hour12?: boolean | null; className?: string; }) { + const use12 = resolveClockHour12(hour12); const hours = time.getHours() % 12; const minutes = time.getMinutes(); const seconds = time.getSeconds(); @@ -310,6 +320,7 @@ export function AnalogClockWidget({ hour: "2-digit", minute: "2-digit", second: "2-digit", + hour12: use12, })}
@@ -365,16 +376,22 @@ export function useHomeClockVisibility(): [boolean, (next: boolean) => void] { return [visible, setVisible]; } -export function Clock({ style, time, lang, size = "md" }: ClockProps) { +export function Clock({ style, time, lang, hour12, size = "md" }: ClockProps) { const resolved = resolveClockStyle(style); + const use12 = resolveClockHour12(hour12); const dayName = formatWeekday(time, lang); const hijriDate = formatHijri(time, lang); const gregorianDate = formatDate(time, lang); - const timeNoSec = formatTime(time, lang, { hour: "2-digit", minute: "2-digit" }); + const timeNoSec = formatTime(time, lang, { + hour: "2-digit", + minute: "2-digit", + hour12: use12, + }); const timeWithSec = formatTime(time, lang, { hour: "2-digit", minute: "2-digit", second: "2-digit", + hour12: use12, }); const timeFontClass = diff --git a/artifacts/teaboy-os/src/lib/i18n-format.ts b/artifacts/teaboy-os/src/lib/i18n-format.ts index 30e6c1fc..0ce5d4df 100644 --- a/artifacts/teaboy-os/src/lib/i18n-format.ts +++ b/artifacts/teaboy-os/src/lib/i18n-format.ts @@ -18,9 +18,10 @@ export function formatTime( options: Intl.DateTimeFormatOptions = { hour: "2-digit", minute: "2-digit" }, ): string { const d = date instanceof Date ? date : new Date(date); + const hour12 = options.hour12 ?? false; return new Intl.DateTimeFormat(withLatn(baseLocale(lang)), { ...options, - hour12: false, + hour12, numberingSystem: "latn", }).format(d); } diff --git a/artifacts/teaboy-os/src/locales/ar.json b/artifacts/teaboy-os/src/locales/ar.json index 0fd74ef7..b87d7432 100644 --- a/artifacts/teaboy-os/src/locales/ar.json +++ b/artifacts/teaboy-os/src/locales/ar.json @@ -81,6 +81,11 @@ "clockStyle": { "label": "نمط الساعة", "showWidget": "إظهار ساعة الحائط في الشاشة الرئيسية", + "hourFormat": { + "label": "صيغة الساعة", + "h12": "12 ساعة (ص/م)", + "h24": "24 ساعة" + }, "options": { "full": "كامل (الوقت واليوم والتقويمان الهجري والميلادي)", "digital": "رقمي مع الثواني", diff --git a/artifacts/teaboy-os/src/locales/en.json b/artifacts/teaboy-os/src/locales/en.json index 55a488cc..215b3f65 100644 --- a/artifacts/teaboy-os/src/locales/en.json +++ b/artifacts/teaboy-os/src/locales/en.json @@ -81,6 +81,11 @@ "clockStyle": { "label": "Clock style", "showWidget": "Show wall clock on home screen", + "hourFormat": { + "label": "Hour format", + "h12": "12-hour (AM/PM)", + "h24": "24-hour" + }, "options": { "full": "Full (time, weekday, Hijri & Gregorian)", "digital": "Digital with seconds", diff --git a/artifacts/teaboy-os/src/pages/home.tsx b/artifacts/teaboy-os/src/pages/home.tsx index c46411c5..ac9d7809 100644 --- a/artifacts/teaboy-os/src/pages/home.tsx +++ b/artifacts/teaboy-os/src/pages/home.tsx @@ -269,12 +269,20 @@ export default function HomePage() { {/* Clock + dates */}
- +
{/* Controls */}
- +
diff --git a/lib/api-client-react/src/generated/api.schemas.ts b/lib/api-client-react/src/generated/api.schemas.ts index 219d986d..77e09f84 100644 --- a/lib/api-client-react/src/generated/api.schemas.ts +++ b/lib/api-client-react/src/generated/api.schemas.ts @@ -83,6 +83,14 @@ export interface UpdateClockStyleBody { clockStyle: ClockStyle | null; } +export interface UpdateClockHour12Body { + /** + * Whether to use 12-hour (AM/PM) clock format; null clears and uses the default (24-hour) + * @nullable + */ + clockHour12: boolean | null; +} + export interface AuthUser { id: number; username: string; @@ -94,6 +102,8 @@ export interface AuthUser { preferredLanguage: string; clockStyle?: ClockStyle | null; /** @nullable */ + clockHour12?: boolean | null; + /** @nullable */ avatarUrl?: string | null; isActive: boolean; roles: string[]; @@ -111,6 +121,8 @@ export interface UserProfile { preferredLanguage: string; clockStyle?: ClockStyle | null; /** @nullable */ + clockHour12?: boolean | null; + /** @nullable */ avatarUrl?: string | null; isActive: boolean; roles: string[]; diff --git a/lib/api-client-react/src/generated/api.ts b/lib/api-client-react/src/generated/api.ts index 2aaa43e0..94757c32 100644 --- a/lib/api-client-react/src/generated/api.ts +++ b/lib/api-client-react/src/generated/api.ts @@ -45,6 +45,7 @@ import type { SuccessResponse, UpdateAppBody, UpdateAppSettingsBody, + UpdateClockHour12Body, UpdateClockStyleBody, UpdateLanguageBody, UpdateMyAppOrderBody, @@ -969,6 +970,92 @@ export const useUpdateClockStyle = < return useMutation(getUpdateClockStyleMutationOptions(options)); }; +/** + * @summary Update preferred 12/24-hour clock format + */ +export const getUpdateClockHour12Url = () => { + return `/api/auth/me/clock-hour12`; +}; + +export const updateClockHour12 = async ( + updateClockHour12Body: UpdateClockHour12Body, + options?: RequestInit, +): Promise => { + return customFetch(getUpdateClockHour12Url(), { + ...options, + method: "PATCH", + headers: { "Content-Type": "application/json", ...options?.headers }, + body: JSON.stringify(updateClockHour12Body), + }); +}; + +export const getUpdateClockHour12MutationOptions = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + const mutationKey = ["updateClockHour12"]; + const { mutation: mutationOptions, request: requestOptions } = options + ? options.mutation && + "mutationKey" in options.mutation && + options.mutation.mutationKey + ? options + : { ...options, mutation: { ...options.mutation, mutationKey } } + : { mutation: { mutationKey }, request: undefined }; + + const mutationFn: MutationFunction< + Awaited>, + { data: BodyType } + > = (props) => { + const { data } = props ?? {}; + + return updateClockHour12(data, requestOptions); + }; + + return { mutationFn, ...mutationOptions }; +}; + +export type UpdateClockHour12MutationResult = NonNullable< + Awaited> +>; +export type UpdateClockHour12MutationBody = BodyType; +export type UpdateClockHour12MutationError = ErrorType; + +/** + * @summary Update preferred 12/24-hour clock format + */ +export const useUpdateClockHour12 = < + TError = ErrorType, + TContext = unknown, +>(options?: { + mutation?: UseMutationOptions< + Awaited>, + TError, + { data: BodyType }, + TContext + >; + request?: SecondParameter; +}): UseMutationResult< + Awaited>, + TError, + { data: BodyType }, + TContext +> => { + return useMutation(getUpdateClockHour12MutationOptions(options)); +}; + /** * @summary List all active apps */ diff --git a/lib/api-spec/openapi.yaml b/lib/api-spec/openapi.yaml index 5b2e57eb..25696612 100644 --- a/lib/api-spec/openapi.yaml +++ b/lib/api-spec/openapi.yaml @@ -270,6 +270,31 @@ paths: schema: $ref: "#/components/schemas/ErrorResponse" + /auth/me/clock-hour12: + patch: + operationId: updateClockHour12 + tags: [auth] + summary: Update preferred 12/24-hour clock format + requestBody: + required: true + content: + application/json: + schema: + $ref: "#/components/schemas/UpdateClockHour12Body" + responses: + "200": + description: Updated + content: + application/json: + schema: + $ref: "#/components/schemas/AuthUser" + "400": + description: Validation error + content: + application/json: + schema: + $ref: "#/components/schemas/ErrorResponse" + # Apps /apps: get: @@ -1025,6 +1050,15 @@ components: required: - clockStyle + UpdateClockHour12Body: + type: object + properties: + clockHour12: + type: ["boolean", "null"] + description: Whether to use 12-hour (AM/PM) clock format; null clears and uses the default (24-hour) + required: + - clockHour12 + AuthUser: type: object properties: @@ -1044,6 +1078,8 @@ components: oneOf: - $ref: "#/components/schemas/ClockStyle" - type: "null" + clockHour12: + type: ["boolean", "null"] avatarUrl: type: ["string", "null"] isActive: @@ -1083,6 +1119,8 @@ components: oneOf: - $ref: "#/components/schemas/ClockStyle" - type: "null" + clockHour12: + type: ["boolean", "null"] avatarUrl: type: ["string", "null"] isActive: diff --git a/lib/api-zod/src/generated/api.ts b/lib/api-zod/src/generated/api.ts index 01838637..bb6c7da9 100644 --- a/lib/api-zod/src/generated/api.ts +++ b/lib/api-zod/src/generated/api.ts @@ -51,6 +51,7 @@ export const LoginResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -130,6 +131,7 @@ export const GetMeResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -158,6 +160,7 @@ export const UpdateLanguageResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -193,6 +196,41 @@ export const UpdateClockStyleResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), + avatarUrl: zod.string().nullish(), + isActive: zod.boolean(), + roles: zod.array(zod.string()), + createdAt: zod.coerce.date(), +}); + +/** + * @summary Update preferred 12/24-hour clock format + */ +export const UpdateClockHour12Body = zod.object({ + clockHour12: zod + .boolean() + .nullable() + .describe( + "Whether to use 12-hour (AM\/PM) clock format; null clears and uses the default (24-hour)", + ), +}); + +export const UpdateClockHour12Response = zod.object({ + id: zod.number(), + username: zod.string(), + email: zod.string(), + displayNameAr: zod.string().nullish(), + displayNameEn: zod.string().nullish(), + preferredLanguage: zod.string(), + clockStyle: zod + .union([ + zod + .enum(["full", "digital", "digital-no-seconds", "analog", "minimal"]) + .describe("Per-user home-screen clock style preference"), + zod.null(), + ]) + .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -741,6 +779,7 @@ export const ListUsersResponseItem = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -786,6 +825,7 @@ export const GetUserResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), @@ -821,6 +861,7 @@ export const UpdateUserResponse = zod.object({ zod.null(), ]) .optional(), + clockHour12: zod.boolean().nullish(), avatarUrl: zod.string().nullish(), isActive: zod.boolean(), roles: zod.array(zod.string()), diff --git a/lib/db/src/schema/users.ts b/lib/db/src/schema/users.ts index 583ae433..5b9c0aaf 100644 --- a/lib/db/src/schema/users.ts +++ b/lib/db/src/schema/users.ts @@ -11,6 +11,7 @@ export const usersTable = pgTable("users", { displayNameEn: varchar("display_name_en", { length: 200 }), preferredLanguage: varchar("preferred_language", { length: 10 }).notNull().default("ar"), clockStyle: varchar("clock_style", { length: 30 }), + clockHour12: boolean("clock_hour12"), avatarUrl: text("avatar_url"), isActive: boolean("is_active").notNull().default(true), createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),