Task #28: Let users pick a 12-hour (AM/PM) clock instead of 24-hour
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.
This commit is contained in:
@@ -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<void> =
|
||||
res.json(buildAuthUser(user, roles));
|
||||
});
|
||||
|
||||
router.patch("/auth/me/clock-hour12", requireAuth, async (req, res): Promise<void> => {
|
||||
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<void> => {
|
||||
const parsed = UpdateClockStyleBody.safeParse(req.body);
|
||||
if (!parsed.success) {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
@@ -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<AuthUser>(getGetMeQueryKey());
|
||||
if (previous) {
|
||||
queryClient.setQueryData<AuthUser>(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 (
|
||||
<Popover open={open} onOpenChange={setOpen}>
|
||||
<PopoverTrigger asChild>
|
||||
@@ -105,6 +136,36 @@ export function ClockStylePicker({ currentStyle }: Props) {
|
||||
</span>
|
||||
</button>
|
||||
<div className="h-px bg-foreground/10 mx-1 mb-1" />
|
||||
<div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t("home.clockStyle.hourFormat.label")}
|
||||
</div>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-1 mx-1 mb-1 p-1 rounded-lg bg-foreground/5"
|
||||
role="group"
|
||||
aria-label={t("home.clockStyle.hourFormat.label")}
|
||||
>
|
||||
{([false, true] as const).map((value) => {
|
||||
const isActive = value === activeHour12;
|
||||
const labelKey = value ? "h12" : "h24";
|
||||
return (
|
||||
<button
|
||||
key={labelKey}
|
||||
type="button"
|
||||
onClick={() => chooseHour12(value)}
|
||||
disabled={updateClockHour12.isPending}
|
||||
aria-pressed={isActive}
|
||||
className={`text-xs font-medium px-2 py-1.5 rounded-md transition-colors ${
|
||||
isActive
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{t(`home.clockStyle.hourFormat.${labelKey}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="h-px bg-foreground/10 mx-1 mb-1" />
|
||||
<div className="px-2 py-1.5 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t("home.clockStyle.label")}
|
||||
</div>
|
||||
@@ -123,7 +184,13 @@ export function ClockStylePicker({ currentStyle }: Props) {
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="shrink-0">
|
||||
<Clock style={style} time={now} lang={lang} size="sm" />
|
||||
<Clock
|
||||
style={style}
|
||||
time={now}
|
||||
lang={lang}
|
||||
hour12={activeHour12}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-xs text-foreground font-medium truncate">
|
||||
{t(`home.clockStyle.options.${style}`)}
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
</div>
|
||||
<div className="text-[11px] text-muted-foreground mt-0.5 tabular-nums">
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -81,6 +81,11 @@
|
||||
"clockStyle": {
|
||||
"label": "نمط الساعة",
|
||||
"showWidget": "إظهار ساعة الحائط في الشاشة الرئيسية",
|
||||
"hourFormat": {
|
||||
"label": "صيغة الساعة",
|
||||
"h12": "12 ساعة (ص/م)",
|
||||
"h24": "24 ساعة"
|
||||
},
|
||||
"options": {
|
||||
"full": "كامل (الوقت واليوم والتقويمان الهجري والميلادي)",
|
||||
"digital": "رقمي مع الثواني",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -269,12 +269,20 @@ export default function HomePage() {
|
||||
|
||||
{/* Clock + dates */}
|
||||
<div className="px-2 shrink-0">
|
||||
<Clock style={user?.clockStyle ?? null} time={time} lang={lang} />
|
||||
<Clock
|
||||
style={user?.clockStyle ?? null}
|
||||
hour12={user?.clockHour12 ?? null}
|
||||
time={time}
|
||||
lang={lang}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="flex items-center gap-1 shrink basis-0 flex-1 justify-end">
|
||||
<ClockStylePicker currentStyle={user?.clockStyle ?? null} />
|
||||
<ClockStylePicker
|
||||
currentStyle={user?.clockStyle ?? null}
|
||||
currentHour12={user?.clockHour12 ?? null}
|
||||
/>
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
aria-label="Toggle language"
|
||||
@@ -316,7 +324,11 @@ export default function HomePage() {
|
||||
</div>
|
||||
{homeClockVisible && (
|
||||
<div className="hidden md:block shrink-0">
|
||||
<AnalogClockWidget time={time} lang={lang} />
|
||||
<AnalogClockWidget
|
||||
time={time}
|
||||
lang={lang}
|
||||
hour12={user?.clockHour12 ?? null}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -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[];
|
||||
|
||||
@@ -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<AuthUser> => {
|
||||
return customFetch<AuthUser>(getUpdateClockHour12Url(), {
|
||||
...options,
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(updateClockHour12Body),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateClockHour12MutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockHour12>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockHour12Body> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockHour12>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockHour12Body> },
|
||||
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<ReturnType<typeof updateClockHour12>>,
|
||||
{ data: BodyType<UpdateClockHour12Body> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateClockHour12(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateClockHour12MutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateClockHour12>>
|
||||
>;
|
||||
export type UpdateClockHour12MutationBody = BodyType<UpdateClockHour12Body>;
|
||||
export type UpdateClockHour12MutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Update preferred 12/24-hour clock format
|
||||
*/
|
||||
export const useUpdateClockHour12 = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockHour12>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockHour12Body> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateClockHour12>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockHour12Body> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateClockHour12MutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary List all active apps
|
||||
*/
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user