Add per-user clock style preference for the home status bar.
Original task #20: Let each user pick their own home-screen clock style (analog/digital/minimal/etc.), persisted on the user record and restored on login / other devices. Default = "full". Changes: - DB: added `clock_style varchar(30)` (nullable) to `users` (lib/db/src/schema/users.ts) and applied via direct ALTER TABLE (drizzle-kit push had unrelated interactive prompts about app_opens / user_sessions which were not safe to answer). - OpenAPI: added `ClockStyle` enum (full/digital/digital-no-seconds /analog/minimal), `UpdateClockStyleBody`, exposed `clockStyle` on AuthUser and UserProfile, and added PATCH /auth/me/clock-style. Regenerated typed client and zod schemas. - API: `buildAuthUser` and `buildUserProfile` include `clockStyle`; new authenticated endpoint updates the current user's style and returns the refreshed AuthUser. - Frontend: new `Clock` component (artifacts/teaboy-os/src/ components/clock.tsx) with five variants sharing a single `useNow` tick hook, plus an SVG analog clock; honors existing Latin-digit/locale formatting helpers. New `ClockStylePicker` (popover) shown in the status bar with a live preview for each option and optimistic update through the AuthUser query cache. - home.tsx replaces the hard-coded clock block with `<Clock>` driven by `user.clockStyle`; trigger button placed next to the language toggle. - i18n: added `home.clockStyle.label` and per-style option labels to en.json and ar.json. Verified via e2e: register → default "full" rendered → switch to analog → reload → analog persists → switch to minimal → time-only renders. RTL layout + Latin digits both correct after language toggle.
This commit is contained in:
@@ -65,6 +65,24 @@ export interface UpdateLanguageBody {
|
||||
language: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Per-user home-screen clock style preference
|
||||
*/
|
||||
export type ClockStyle = (typeof ClockStyle)[keyof typeof ClockStyle];
|
||||
|
||||
export const ClockStyle = {
|
||||
full: "full",
|
||||
digital: "digital",
|
||||
"digital-no-seconds": "digital-no-seconds",
|
||||
analog: "analog",
|
||||
minimal: "minimal",
|
||||
} as const;
|
||||
|
||||
export interface UpdateClockStyleBody {
|
||||
/** Clock style; pass null to clear and use the default */
|
||||
clockStyle: ClockStyle | null;
|
||||
}
|
||||
|
||||
export interface AuthUser {
|
||||
id: number;
|
||||
username: string;
|
||||
@@ -74,6 +92,7 @@ export interface AuthUser {
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
preferredLanguage: string;
|
||||
clockStyle?: ClockStyle | null;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
@@ -90,6 +109,7 @@ export interface UserProfile {
|
||||
/** @nullable */
|
||||
displayNameEn?: string | null;
|
||||
preferredLanguage: string;
|
||||
clockStyle?: ClockStyle | null;
|
||||
/** @nullable */
|
||||
avatarUrl?: string | null;
|
||||
isActive: boolean;
|
||||
|
||||
@@ -45,6 +45,7 @@ import type {
|
||||
SuccessResponse,
|
||||
UpdateAppBody,
|
||||
UpdateAppSettingsBody,
|
||||
UpdateClockStyleBody,
|
||||
UpdateLanguageBody,
|
||||
UpdateMyAppOrderBody,
|
||||
UpdateServiceBody,
|
||||
@@ -882,6 +883,92 @@ export const useUpdateLanguage = <
|
||||
return useMutation(getUpdateLanguageMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary Update preferred home-screen clock style
|
||||
*/
|
||||
export const getUpdateClockStyleUrl = () => {
|
||||
return `/api/auth/me/clock-style`;
|
||||
};
|
||||
|
||||
export const updateClockStyle = async (
|
||||
updateClockStyleBody: UpdateClockStyleBody,
|
||||
options?: RequestInit,
|
||||
): Promise<AuthUser> => {
|
||||
return customFetch<AuthUser>(getUpdateClockStyleUrl(), {
|
||||
...options,
|
||||
method: "PATCH",
|
||||
headers: { "Content-Type": "application/json", ...options?.headers },
|
||||
body: JSON.stringify(updateClockStyleBody),
|
||||
});
|
||||
};
|
||||
|
||||
export const getUpdateClockStyleMutationOptions = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockStyle>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockStyleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockStyle>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockStyleBody> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ["updateClockStyle"];
|
||||
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 updateClockStyle>>,
|
||||
{ data: BodyType<UpdateClockStyleBody> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return updateClockStyle(data, requestOptions);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type UpdateClockStyleMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof updateClockStyle>>
|
||||
>;
|
||||
export type UpdateClockStyleMutationBody = BodyType<UpdateClockStyleBody>;
|
||||
export type UpdateClockStyleMutationError = ErrorType<ErrorResponse>;
|
||||
|
||||
/**
|
||||
* @summary Update preferred home-screen clock style
|
||||
*/
|
||||
export const useUpdateClockStyle = <
|
||||
TError = ErrorType<ErrorResponse>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof updateClockStyle>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockStyleBody> },
|
||||
TContext
|
||||
>;
|
||||
request?: SecondParameter<typeof customFetch>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof updateClockStyle>>,
|
||||
TError,
|
||||
{ data: BodyType<UpdateClockStyleBody> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getUpdateClockStyleMutationOptions(options));
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary List all active apps
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user