Task #527: Unify topbar prefs into per-user Settings panel
- New `settings-panel.tsx` consolidates four duplicate topbar buttons
(clock-style picker, quick-mute, notification-settings popover,
language-toggle globe) behind a single gear icon that opens a
right-side Sheet (full-width on mobile, capped on desktop).
- Panel groups: Language (ar/en), Notifications (per-slot
enabled/sound/vibration), Sound (master mute), Clock (visibility,
12/24h, style).
- Reuses existing hooks (useUpdateLanguage, useUpdate{Clock*},
useUpdatePrefs, useHome/TopbarClockVisibility) so all persistence
paths (DB columns + localStorage) stay identical — no migration.
- home.tsx topbar now renders only inbox link, single notification
bell (kept as nav link), Settings gear, and logout.
- New i18n keys under `settingsPanel.*` in ar.json + en.json.
- Old ClockStylePicker / NotificationSettingsPicker / QuickMuteButton
exports remain in their original files (unused) — kept to minimize
diff scope; can be removed in a follow-up.
This commit is contained in:
@@ -0,0 +1,540 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
useUpdateLanguage,
|
||||
useUpdateClockStyle,
|
||||
useUpdateClockHour12,
|
||||
getGetMeQueryKey,
|
||||
type AuthUser,
|
||||
type ClockStyle as ClockStyleType,
|
||||
type NotificationSoundId,
|
||||
} from "@workspace/api-client-react";
|
||||
import {
|
||||
Settings as SettingsIcon,
|
||||
BellOff,
|
||||
Volume2,
|
||||
VolumeX,
|
||||
Check,
|
||||
Eye,
|
||||
EyeOff,
|
||||
Play,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
Clock,
|
||||
CLOCK_STYLES,
|
||||
resolveClockStyle,
|
||||
resolveClockHour12,
|
||||
useNow,
|
||||
useHomeClockVisibility,
|
||||
useTopbarClockVisibility,
|
||||
} from "@/components/clock";
|
||||
import {
|
||||
ALL_SOUND_IDS,
|
||||
notificationPlayer,
|
||||
} from "@/lib/notification-sounds";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
import { useUpdatePrefs } from "@/components/notification-settings";
|
||||
import i18n from "@/i18n";
|
||||
|
||||
type Slot = "order" | "meeting" | "note";
|
||||
|
||||
const SLOT_KEYS: Record<
|
||||
Slot,
|
||||
{
|
||||
enabledKey: keyof Pick<
|
||||
AuthUser,
|
||||
"notifyOrdersEnabled" | "notifyMeetingsEnabled" | "notifyNotesEnabled"
|
||||
>;
|
||||
soundKey: keyof Pick<
|
||||
AuthUser,
|
||||
"notificationSoundOrder" | "notificationSoundMeeting" | "notificationSoundNote"
|
||||
>;
|
||||
vibrateKey: keyof Pick<
|
||||
AuthUser,
|
||||
"vibrationEnabledOrder" | "vibrationEnabledMeeting" | "vibrationEnabledNote"
|
||||
>;
|
||||
enabledLabel: string;
|
||||
vibrateLabel: string;
|
||||
}
|
||||
> = {
|
||||
order: {
|
||||
enabledKey: "notifyOrdersEnabled",
|
||||
soundKey: "notificationSoundOrder",
|
||||
vibrateKey: "vibrationEnabledOrder",
|
||||
enabledLabel: "notifSettings.ordersEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationOrder",
|
||||
},
|
||||
meeting: {
|
||||
enabledKey: "notifyMeetingsEnabled",
|
||||
soundKey: "notificationSoundMeeting",
|
||||
vibrateKey: "vibrationEnabledMeeting",
|
||||
enabledLabel: "notifSettings.meetingsEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationMeeting",
|
||||
},
|
||||
note: {
|
||||
enabledKey: "notifyNotesEnabled",
|
||||
soundKey: "notificationSoundNote",
|
||||
vibrateKey: "vibrationEnabledNote",
|
||||
enabledLabel: "notifSettings.notesEnabled",
|
||||
vibrateLabel: "notifSettings.vibrationNote",
|
||||
},
|
||||
};
|
||||
|
||||
function ToggleRow({
|
||||
active,
|
||||
onClick,
|
||||
label,
|
||||
leftIcon,
|
||||
}: {
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
label: string;
|
||||
leftIcon?: React.ReactNode;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
role="switch"
|
||||
aria-checked={active}
|
||||
aria-label={label}
|
||||
onClick={onClick}
|
||||
className="w-full flex items-center justify-between gap-3 px-3 py-2.5 rounded-lg text-start hover:bg-foreground/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{leftIcon}
|
||||
<span className="text-sm text-foreground font-medium truncate">{label}</span>
|
||||
</div>
|
||||
<span
|
||||
className={`relative inline-flex h-5 w-9 shrink-0 items-center rounded-full transition-colors ${
|
||||
active ? "bg-primary" : "bg-foreground/15"
|
||||
}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white shadow transition-transform ${
|
||||
active
|
||||
? "translate-x-[18px] rtl:-translate-x-[18px]"
|
||||
: "translate-x-0.5 rtl:-translate-x-0.5"
|
||||
}`}
|
||||
/>
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function SectionTitle({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="px-2 pb-1.5 pt-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupCard({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="rounded-xl border border-foreground/10 bg-background/40 p-2">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LanguageGroup() {
|
||||
const { t, i18n: i18nInstance } = useTranslation();
|
||||
const queryClient = useQueryClient();
|
||||
const updateLanguage = useUpdateLanguage();
|
||||
const current = i18nInstance.language === "ar" ? "ar" : "en";
|
||||
|
||||
const choose = (next: "ar" | "en") => {
|
||||
if (next === current) return;
|
||||
i18n.changeLanguage(next);
|
||||
updateLanguage.mutate(
|
||||
{ data: { language: next } },
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const options: Array<{ value: "ar" | "en"; label: string }> = [
|
||||
{ value: "ar", label: t("settingsPanel.lang.ar") },
|
||||
{ value: "en", label: t("settingsPanel.lang.en") },
|
||||
];
|
||||
|
||||
return (
|
||||
<GroupCard>
|
||||
<SectionTitle>{t("settingsPanel.section.language")}</SectionTitle>
|
||||
<div
|
||||
className="grid grid-cols-2 gap-1 mx-1 mb-1 p-1 rounded-lg bg-foreground/5"
|
||||
role="radiogroup"
|
||||
aria-label={t("settingsPanel.section.language")}
|
||||
>
|
||||
{options.map((opt) => {
|
||||
const isActive = opt.value === current;
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="radio"
|
||||
aria-checked={isActive}
|
||||
onClick={() => choose(opt.value)}
|
||||
data-testid={`settings-lang-${opt.value}`}
|
||||
className={`text-sm font-medium px-2 py-1.5 rounded-md transition-colors ${
|
||||
isActive
|
||||
? "bg-background text-foreground shadow-sm"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
}`}
|
||||
>
|
||||
{opt.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</GroupCard>
|
||||
);
|
||||
}
|
||||
|
||||
function SoundList({
|
||||
current,
|
||||
onPick,
|
||||
}: {
|
||||
current: NotificationSoundId;
|
||||
onPick: (id: NotificationSoundId) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<div className="flex flex-col gap-0.5 max-h-56 overflow-y-auto">
|
||||
{ALL_SOUND_IDS.map((id) => {
|
||||
const isActive = id === current;
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
className={`flex items-center justify-between gap-2 px-2 py-1.5 rounded-lg ${
|
||||
isActive ? "bg-primary/10" : "hover:bg-foreground/5"
|
||||
}`}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onPick(id)}
|
||||
className="flex-1 flex items-center gap-2 text-start min-w-0"
|
||||
>
|
||||
{isActive ? (
|
||||
<Check size={14} className="text-primary shrink-0" />
|
||||
) : (
|
||||
<span className="w-3.5 shrink-0" />
|
||||
)}
|
||||
<span className="text-sm text-foreground font-medium truncate">
|
||||
{t(`notifSettings.sounds.${id}`)}
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => notificationPlayer.testPlay(id)}
|
||||
aria-label={t("notifSettings.preview")}
|
||||
className="p-1 rounded hover:bg-foreground/10 text-muted-foreground"
|
||||
>
|
||||
<Play size={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NotificationsGroup() {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const apply = useUpdatePrefs();
|
||||
const [slot, setSlot] = useState<Slot>("order");
|
||||
if (!user) return null;
|
||||
const cfg = SLOT_KEYS[slot];
|
||||
return (
|
||||
<GroupCard>
|
||||
<SectionTitle>{t("settingsPanel.section.notifications")}</SectionTitle>
|
||||
<div
|
||||
className="grid grid-cols-3 gap-1 mx-1 mb-1 p-1 rounded-lg bg-foreground/5"
|
||||
role="group"
|
||||
>
|
||||
{(["order", "meeting", "note"] as const).map((s) => {
|
||||
const isActive = s === slot;
|
||||
return (
|
||||
<button
|
||||
key={s}
|
||||
type="button"
|
||||
onClick={() => setSlot(s)}
|
||||
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(`notifSettings.slot.${s}`)}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<ToggleRow
|
||||
active={user[cfg.enabledKey]}
|
||||
onClick={() =>
|
||||
apply({ [cfg.enabledKey]: !user[cfg.enabledKey] } as Record<string, boolean>)
|
||||
}
|
||||
label={t(cfg.enabledLabel)}
|
||||
/>
|
||||
<ToggleRow
|
||||
active={user[cfg.vibrateKey]}
|
||||
onClick={() =>
|
||||
apply({ [cfg.vibrateKey]: !user[cfg.vibrateKey] } as Record<string, boolean>)
|
||||
}
|
||||
label={t(cfg.vibrateLabel)}
|
||||
/>
|
||||
<div className="h-px bg-foreground/10 mx-1 my-1" />
|
||||
<SoundList
|
||||
current={user[cfg.soundKey]}
|
||||
onPick={(id) => apply({ [cfg.soundKey]: id } as Record<string, NotificationSoundId>)}
|
||||
/>
|
||||
</GroupCard>
|
||||
);
|
||||
}
|
||||
|
||||
function SoundGroup() {
|
||||
const { t } = useTranslation();
|
||||
const { user } = useAuth();
|
||||
const apply = useUpdatePrefs();
|
||||
if (!user) return null;
|
||||
const muted = user.notificationsMuted;
|
||||
return (
|
||||
<GroupCard>
|
||||
<SectionTitle>{t("settingsPanel.section.sound")}</SectionTitle>
|
||||
<ToggleRow
|
||||
active={!muted}
|
||||
onClick={() => apply({ notificationsMuted: !muted })}
|
||||
label={t("settingsPanel.sound.master")}
|
||||
leftIcon={
|
||||
muted ? (
|
||||
<VolumeX size={16} className="text-muted-foreground shrink-0" />
|
||||
) : (
|
||||
<Volume2 size={16} className="text-primary shrink-0" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</GroupCard>
|
||||
);
|
||||
}
|
||||
|
||||
function ClockGroup() {
|
||||
const { t, i18n: i18nInstance } = useTranslation();
|
||||
const lang = i18nInstance.language;
|
||||
const queryClient = useQueryClient();
|
||||
const now = useNow();
|
||||
const { user } = useAuth();
|
||||
const updateClockStyle = useUpdateClockStyle();
|
||||
const updateClockHour12 = useUpdateClockHour12();
|
||||
const [homeClockVisible, setHomeClockVisible] = useHomeClockVisibility();
|
||||
const [topbarClockVisible, setTopbarClockVisible] = useTopbarClockVisibility();
|
||||
|
||||
if (!user) return null;
|
||||
const active = resolveClockStyle(user.clockStyle);
|
||||
const activeHour12 = resolveClockHour12(user.clockHour12);
|
||||
|
||||
const chooseStyle = (style: ClockStyleType) => {
|
||||
const previous = queryClient.getQueryData<AuthUser>(getGetMeQueryKey());
|
||||
if (previous) {
|
||||
queryClient.setQueryData<AuthUser>(getGetMeQueryKey(), { ...previous, clockStyle: style });
|
||||
}
|
||||
updateClockStyle.mutate(
|
||||
{ data: { clockStyle: style } },
|
||||
{
|
||||
onSuccess: (data) => queryClient.setQueryData(getGetMeQueryKey(), data),
|
||||
onError: () => {
|
||||
if (previous) queryClient.setQueryData(getGetMeQueryKey(), previous);
|
||||
else queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() });
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
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 (
|
||||
<GroupCard>
|
||||
<SectionTitle>{t("settingsPanel.section.clock")}</SectionTitle>
|
||||
<ToggleRow
|
||||
active={homeClockVisible}
|
||||
onClick={() => setHomeClockVisible(!homeClockVisible)}
|
||||
label={t("home.clockStyle.showWidget")}
|
||||
leftIcon={
|
||||
homeClockVisible ? (
|
||||
<Eye size={16} className="text-primary shrink-0" />
|
||||
) : (
|
||||
<EyeOff size={16} className="text-muted-foreground shrink-0" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<ToggleRow
|
||||
active={topbarClockVisible}
|
||||
onClick={() => setTopbarClockVisible(!topbarClockVisible)}
|
||||
label={t("home.clockStyle.showTopbar")}
|
||||
leftIcon={
|
||||
topbarClockVisible ? (
|
||||
<Eye size={16} className="text-primary shrink-0" />
|
||||
) : (
|
||||
<EyeOff size={16} className="text-muted-foreground shrink-0" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
<div className="h-px bg-foreground/10 mx-1 my-1" />
|
||||
<div className="px-2 py-1 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-sm 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 my-1" />
|
||||
<div className="px-2 py-1 text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{t("home.clockStyle.label")}
|
||||
</div>
|
||||
<div className="flex flex-col gap-1 max-h-56 overflow-y-auto">
|
||||
{CLOCK_STYLES.map((style) => {
|
||||
const isActive = style === active;
|
||||
return (
|
||||
<button
|
||||
key={style}
|
||||
type="button"
|
||||
onClick={() => chooseStyle(style)}
|
||||
disabled={updateClockStyle.isPending}
|
||||
className={`flex items-center justify-between gap-3 px-2 py-2 rounded-lg text-start hover:bg-foreground/5 transition-colors ${
|
||||
isActive ? "bg-primary/10" : ""
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3 min-w-0 flex-1">
|
||||
<div className="shrink-0">
|
||||
<Clock
|
||||
style={style}
|
||||
time={now}
|
||||
lang={lang}
|
||||
hour12={activeHour12}
|
||||
size="sm"
|
||||
/>
|
||||
</div>
|
||||
<div className="text-sm text-foreground font-medium truncate">
|
||||
{t(`home.clockStyle.options.${style}`)}
|
||||
</div>
|
||||
</div>
|
||||
{isActive && <Check size={14} className="text-primary shrink-0" />}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</GroupCard>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Unified per-user Settings panel (Task #527). Consolidates the four
|
||||
* preferences that previously each had their own topbar button:
|
||||
* - Language (was: globe button)
|
||||
* - Notifications: per-category sound + vibration (was: bell #2 popover)
|
||||
* - Sound: master mute toggle (was: volume button)
|
||||
* - Clock: visibility, 12/24h, style (was: clock-style picker button)
|
||||
*
|
||||
* Renders as a right-side Sheet on every viewport — full-width on
|
||||
* phones, capped width on desktop. Keeps the same persistence paths
|
||||
* as the original controls (DB columns via the existing hooks +
|
||||
* localStorage for clock visibility), so no migration is required.
|
||||
*/
|
||||
export function SettingsPanel() {
|
||||
const { t } = useTranslation();
|
||||
const [open, setOpen] = useState(false);
|
||||
const { user } = useAuth();
|
||||
if (!user) return null;
|
||||
const muted = user.notificationsMuted;
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t("settingsPanel.title")}
|
||||
aria-haspopup="dialog"
|
||||
data-testid="topbar-settings-button"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors p-1.5 md:p-2.5 rounded-lg md:rounded-xl hover:bg-foreground/5 relative"
|
||||
>
|
||||
<SettingsIcon className="w-[18px] h-[18px] md:w-6 md:h-6" />
|
||||
{muted && (
|
||||
<BellOff
|
||||
aria-hidden="true"
|
||||
className="absolute -top-0.5 -right-0.5 w-3 h-3 text-destructive bg-background rounded-full p-px"
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</SheetTrigger>
|
||||
<SheetContent
|
||||
side="right"
|
||||
className="w-full sm:max-w-md p-0 flex flex-col"
|
||||
data-testid="settings-panel"
|
||||
>
|
||||
<SheetHeader className="px-4 pt-4 pb-2 shrink-0 border-b border-foreground/10">
|
||||
<SheetTitle className="flex items-center gap-2 text-base">
|
||||
<SettingsIcon size={18} />
|
||||
{t("settingsPanel.title")}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="flex-1 overflow-y-auto p-3 space-y-3">
|
||||
<LanguageGroup />
|
||||
<NotificationsGroup />
|
||||
<SoundGroup />
|
||||
<ClockGroup />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
@@ -204,6 +204,22 @@
|
||||
"deleteFailed": "تعذّر حذف الطلبات",
|
||||
"deletePartial": "تم حذف {{deleted}} من {{total}}، تعذّر حذف الباقي"
|
||||
},
|
||||
"settingsPanel": {
|
||||
"title": "الإعدادات",
|
||||
"section": {
|
||||
"language": "اللغة",
|
||||
"notifications": "الإشعارات",
|
||||
"sound": "الصوت",
|
||||
"clock": "الساعة"
|
||||
},
|
||||
"lang": {
|
||||
"ar": "العربية",
|
||||
"en": "English"
|
||||
},
|
||||
"sound": {
|
||||
"master": "تشغيل أصوات الإشعارات"
|
||||
}
|
||||
},
|
||||
"notifSettings": {
|
||||
"title": "أصوات الإشعارات",
|
||||
"mute": "كتم الإشعارات",
|
||||
|
||||
@@ -210,6 +210,22 @@
|
||||
"noNotifications": "No new notifications",
|
||||
"unread": "Unread"
|
||||
},
|
||||
"settingsPanel": {
|
||||
"title": "Settings",
|
||||
"section": {
|
||||
"language": "Language",
|
||||
"notifications": "Notifications",
|
||||
"sound": "Sound",
|
||||
"clock": "Clock"
|
||||
},
|
||||
"lang": {
|
||||
"ar": "العربية",
|
||||
"en": "English"
|
||||
},
|
||||
"sound": {
|
||||
"master": "Play notification sounds"
|
||||
}
|
||||
},
|
||||
"notifSettings": {
|
||||
"title": "Notification sounds",
|
||||
"mute": "Mute notifications",
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
useListIncomingServiceOrders,
|
||||
getListIncomingServiceOrdersQueryKey,
|
||||
useLogout,
|
||||
useUpdateLanguage,
|
||||
useUpdateMyAppOrder,
|
||||
logAppOpen,
|
||||
getGetMeQueryKey,
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
Bell,
|
||||
Inbox,
|
||||
LogOut,
|
||||
Globe,
|
||||
Grid2X2,
|
||||
Coffee,
|
||||
BellRing,
|
||||
@@ -33,7 +31,6 @@ import {
|
||||
} from "lucide-react";
|
||||
import * as LucideIcons from "lucide-react";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import i18n from "@/i18n";
|
||||
import {
|
||||
DndContext,
|
||||
PointerSensor,
|
||||
@@ -66,11 +63,7 @@ import {
|
||||
useHomeClockPosition,
|
||||
type HomeClockSize,
|
||||
} from "@/components/clock";
|
||||
import { ClockStylePicker } from "@/components/clock-style-picker";
|
||||
import {
|
||||
NotificationSettingsPicker,
|
||||
QuickMuteButton,
|
||||
} from "@/components/notification-settings";
|
||||
import { SettingsPanel } from "@/components/settings-panel";
|
||||
|
||||
type IconName = keyof typeof LucideIcons;
|
||||
function isIconName(x: string): x is IconName {
|
||||
@@ -479,7 +472,6 @@ export default function HomePage() {
|
||||
const notesPulse = notePopupQueueLength > 0;
|
||||
|
||||
const logout = useLogout();
|
||||
const updateLanguage = useUpdateLanguage();
|
||||
|
||||
const openApp = (app: App) => {
|
||||
// Fire-and-forget: keepalive ensures the request survives any
|
||||
@@ -519,15 +511,6 @@ export default function HomePage() {
|
||||
});
|
||||
};
|
||||
|
||||
const toggleLanguage = () => {
|
||||
const newLang = lang === "ar" ? "en" : "ar";
|
||||
i18n.changeLanguage(newLang);
|
||||
updateLanguage.mutate(
|
||||
{ data: { language: newLang } },
|
||||
{ onSuccess: () => queryClient.invalidateQueries({ queryKey: getGetMeQueryKey() }) },
|
||||
);
|
||||
};
|
||||
|
||||
const dockApps = (orderedApps ?? apps)?.filter((a) => ["services", "notifications", "admin"].includes(a.slug)) ?? [];
|
||||
|
||||
const initials = (displayName || "?").trim().slice(0, 1).toUpperCase();
|
||||
@@ -582,20 +565,17 @@ export default function HomePage() {
|
||||
numeric `size` prop and use Tailwind classes so the icon
|
||||
itself can scale at the `md:` breakpoint.
|
||||
*/}
|
||||
{/*
|
||||
Task #527: the topbar's right-side cluster used to host five
|
||||
icon buttons (clock-style, quick-mute, notification-settings,
|
||||
language toggle, plus the inbox/notifications/logout below).
|
||||
Per-user UI preferences (language, notification sound +
|
||||
vibration, master sound, clock style/visibility/12-24h) now
|
||||
live behind a single Settings (gear) entry that opens the
|
||||
unified panel. The notification-center bell stays as a
|
||||
navigation link — it's the only bell on the page now.
|
||||
*/}
|
||||
<div className="flex items-center gap-1 md:gap-2 shrink basis-0 flex-1 justify-end">
|
||||
<ClockStylePicker
|
||||
currentStyle={user?.clockStyle ?? null}
|
||||
currentHour12={user?.clockHour12 ?? null}
|
||||
/>
|
||||
<QuickMuteButton />
|
||||
<NotificationSettingsPicker />
|
||||
<button
|
||||
onClick={toggleLanguage}
|
||||
aria-label="Toggle language"
|
||||
className="text-muted-foreground hover:text-foreground transition-colors p-1.5 md:p-2.5 rounded-lg md:rounded-xl hover:bg-foreground/5"
|
||||
>
|
||||
<Globe className="w-[18px] h-[18px] md:w-6 md:h-6" />
|
||||
</button>
|
||||
{canReceiveOrders && (
|
||||
<button
|
||||
onClick={() => setLocation("/orders/incoming")}
|
||||
@@ -621,6 +601,7 @@ export default function HomePage() {
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
<SettingsPanel />
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
className="p-1.5 md:p-2.5 rounded-lg md:rounded-xl hover:bg-foreground/5 text-muted-foreground hover:text-foreground transition-colors"
|
||||
|
||||
Reference in New Issue
Block a user