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:
@@ -15,6 +15,7 @@ import {
|
||||
RegisterBody,
|
||||
LoginBody,
|
||||
UpdateLanguageBody,
|
||||
UpdateClockStyleBody,
|
||||
ForgotPasswordBody,
|
||||
ResetPasswordBody,
|
||||
VerifyResetTokenBody,
|
||||
@@ -36,6 +37,7 @@ function buildAuthUser(user: typeof usersTable.$inferSelect, roles: string[]) {
|
||||
displayNameAr: user.displayNameAr,
|
||||
displayNameEn: user.displayNameEn,
|
||||
preferredLanguage: user.preferredLanguage,
|
||||
clockStyle: user.clockStyle,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isActive: user.isActive,
|
||||
roles,
|
||||
@@ -324,4 +326,26 @@ router.patch("/auth/me/language", requireAuth, async (req, res): Promise<void> =
|
||||
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) {
|
||||
res.status(400).json({ error: parsed.error.message });
|
||||
return;
|
||||
}
|
||||
|
||||
const [user] = await db
|
||||
.update(usersTable)
|
||||
.set({ clockStyle: parsed.data.clockStyle ?? 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));
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -37,6 +37,7 @@ function buildUserProfile(user: typeof usersTable.$inferSelect, roles: string[])
|
||||
displayNameAr: user.displayNameAr,
|
||||
displayNameEn: user.displayNameEn,
|
||||
preferredLanguage: user.preferredLanguage,
|
||||
clockStyle: user.clockStyle,
|
||||
avatarUrl: user.avatarUrl,
|
||||
isActive: user.isActive,
|
||||
roles,
|
||||
|
||||
Reference in New Issue
Block a user