UI: #334 add config page language setting (#374)

* UI: #334 add config page language setting

* temporary remove logout button

* Unify fullscreen view class for dialog expanded and settings view

- Replace dialog-expanded and settings-view with single fullscreen-view class
- Update ResponsiveDialog to use fullscreen-view class
- Update CalendarLayout to use fullscreen-view class for settings view
- Consolidate CSS rules in Menubar.styl to use fullscreen-view
- Update test cases to use new class name

---------

Co-authored-by: Lê Nhân Phụng <lenhanphung@Phung-Mac-M4.local>
This commit is contained in:
lenhanphung
2025-11-28 20:25:06 +07:00
committed by GitHub
parent 6fcf22a1cf
commit 1a85afc57d
15 changed files with 801 additions and 58 deletions
+108 -10
View File
@@ -409,7 +409,7 @@ describe("Menubar interaction with expanded Dialog", () => {
}); });
afterEach(() => { afterEach(() => {
document.body.classList.remove("dialog-expanded"); document.body.classList.remove("fullscreen-view");
}); });
it("has navigation controls element with correct class", () => { it("has navigation controls element with correct class", () => {
@@ -497,7 +497,7 @@ describe("Menubar interaction with expanded Dialog", () => {
const mockOnRefresh = jest.fn(); const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15"); const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded"); document.body.classList.add("fullscreen-view");
renderWithProviders( renderWithProviders(
<Menubar <Menubar
@@ -519,7 +519,7 @@ describe("Menubar interaction with expanded Dialog", () => {
const mockOnRefresh = jest.fn(); const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15"); const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded"); document.body.classList.add("fullscreen-view");
renderWithProviders( renderWithProviders(
<Menubar <Menubar
@@ -538,13 +538,11 @@ describe("Menubar interaction with expanded Dialog", () => {
expect(screen.getByText("Twake")).toBeInTheDocument(); expect(screen.getByText("Twake")).toBeInTheDocument();
}); });
it("keeps avatar clickable when dialog is expanded", async () => { it("opens user dropdown menu when clicking avatar", async () => {
const mockCalendarRef = { current: null }; const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn(); const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15"); const mockCurrentDate = new Date("2024-04-15");
document.body.classList.add("dialog-expanded");
renderWithProviders( renderWithProviders(
<Menubar <Menubar
calendarRef={mockCalendarRef} calendarRef={mockCalendarRef}
@@ -561,10 +559,110 @@ describe("Menubar interaction with expanded Dialog", () => {
fireEvent.click(avatar.closest("button")!); fireEvent.click(avatar.closest("button")!);
await waitFor(() => { await waitFor(() => {
const languageSelector = screen.getByLabelText( expect(screen.getByText("John Doe")).toBeInTheDocument();
"menubar.languageSelector" expect(screen.getByText("test@test.com")).toBeInTheDocument();
); });
expect(languageSelector).toBeInTheDocument(); });
it("closes user dropdown menu when clicking outside", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
});
// Click outside the menu - MUI Menu closes on backdrop click
const backdrop = document.querySelector(".MuiBackdrop-root");
if (backdrop) {
fireEvent.click(backdrop);
} else {
// Fallback: click on body
fireEvent.mouseDown(document.body);
}
await waitFor(
() => {
expect(screen.queryByText("John Doe")).not.toBeInTheDocument();
},
{ timeout: 2000 }
);
});
it("displays user name and email in dropdown menu", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedState
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText("John Doe")).toBeInTheDocument();
expect(screen.getByText("test@test.com")).toBeInTheDocument();
});
});
it("navigates to settings page when clicking Settings in dropdown", async () => {
const mockCalendarRef = { current: null };
const mockOnRefresh = jest.fn();
const mockCurrentDate = new Date("2024-04-15");
const preloadedStateWithSettings = {
...preloadedState,
settings: {
language: "en",
view: "calendar",
},
};
const { store } = renderWithProviders(
<Menubar
calendarRef={mockCalendarRef}
onRefresh={mockOnRefresh}
currentDate={mockCurrentDate}
currentView="dayGridMonth"
/>,
preloadedStateWithSettings
);
const avatar = screen.getByText("JD");
fireEvent.click(avatar.closest("button")!);
await waitFor(() => {
expect(screen.getByText(/Settings/i)).toBeInTheDocument();
});
const settingsButton = screen.getByText(/Settings/i);
fireEvent.click(settingsButton);
await waitFor(() => {
const state = store.getState();
expect(state.settings.view).toBe("settings");
}); });
}); });
@@ -0,0 +1,158 @@
import { fireEvent, screen, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import SettingsPage from "../../../src/features/Settings/SettingsPage";
import { renderWithProviders } from "../../utils/Renderwithproviders";
describe("SettingsPage", () => {
const preloadedState = {
user: {
userData: {
sub: "test",
email: "test@test.com",
family_name: "Doe",
name: "John",
sid: "mockSid",
openpaasId: "667037022b752d0026472254",
},
},
settings: {
language: "en",
view: "settings",
},
};
it("renders settings page with sidebar and content layout", () => {
renderWithProviders(<SettingsPage />, preloadedState);
expect(screen.getByRole("main")).toHaveClass(
"main-layout",
"settings-layout"
);
});
it("displays sidebar navigation with Settings and Notifications items", () => {
const { container } = renderWithProviders(<SettingsPage />, preloadedState);
// Check sidebar navigation items
const sidebar = container.querySelector(".settings-sidebar");
expect(sidebar).toBeInTheDocument();
expect(screen.getAllByText(/settings.title/i).length).toBeGreaterThan(0);
expect(screen.getByText(/settings.notifications/i)).toBeInTheDocument();
});
it("highlights active navigation item", () => {
const { container } = renderWithProviders(<SettingsPage />, preloadedState);
const settingsNavItem = container.querySelector(
".settings-nav-item.active"
);
expect(settingsNavItem).toBeInTheDocument();
});
it("navigates back to calendar view when clicking back button", async () => {
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
const backButton = screen.getByLabelText("settings.back");
fireEvent.click(backButton);
await waitFor(() => {
const state = store.getState();
expect(state.settings.view).toBe("calendar");
});
});
it("switches between Settings and Notifications tabs", () => {
renderWithProviders(<SettingsPage />, preloadedState);
const notificationsTab = screen.getByRole("tab", {
name: /settings.notifications/i,
});
fireEvent.click(notificationsTab);
expect(
screen.getByText(/settings.notifications.empty/i)
).toBeInTheDocument();
});
it("displays language select in Settings tab", () => {
renderWithProviders(<SettingsPage />, preloadedState);
expect(
screen.getByLabelText("settings.languageSelector")
).toBeInTheDocument();
});
it("displays all available language options", async () => {
renderWithProviders(<SettingsPage />, preloadedState);
const languageSelect = screen.getByLabelText("settings.languageSelector");
// Click on the select to open dropdown
fireEvent.mouseDown(languageSelect);
// Wait for menu to appear - MUI Select uses Menu internally
// Note: In test environment, Select may not open menu, so we verify Select exists and has correct value
expect(languageSelect).toBeInTheDocument();
expect(languageSelect).toHaveTextContent("English");
});
it("dispatches setLanguage action when language is changed", async () => {
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
const languageSelect = screen.getByLabelText("settings.languageSelector");
// MUI Select uses a native input element - find and change it
const nativeInput = languageSelect.querySelector(
'input[aria-hidden="true"]'
) as HTMLInputElement;
expect(nativeInput).toBeInTheDocument();
// Simulate change event on the native input
Object.defineProperty(nativeInput, "value", {
writable: true,
value: "fr",
});
fireEvent.change(nativeInput, { target: { value: "fr" } });
await waitFor(() => {
const state = store.getState();
expect(state.settings.language).toBe("fr");
});
});
it("saves language change to localStorage", async () => {
const { store } = renderWithProviders(<SettingsPage />, preloadedState);
const languageSelect = screen.getByLabelText("settings.languageSelector");
// MUI Select uses a native input element - find and change it
const nativeInput = languageSelect.querySelector(
'input[aria-hidden="true"]'
) as HTMLInputElement;
expect(nativeInput).toBeInTheDocument();
// Simulate change event on the native input
Object.defineProperty(nativeInput, "value", {
writable: true,
value: "fr",
});
fireEvent.change(nativeInput, { target: { value: "fr" } });
await waitFor(() => {
expect(localStorage.getItem("lang")).toBe("fr");
});
});
it("shows empty state in Notifications tab", () => {
renderWithProviders(<SettingsPage />, preloadedState);
const notificationsTab = screen.getByRole("tab", {
name: /settings.notifications/i,
});
fireEvent.click(notificationsTab);
expect(
screen.getByText("settings.notifications.empty")
).toBeInTheDocument();
});
});
+92
View File
@@ -0,0 +1,92 @@
import { getUserDisplayName } from "../../src/utils/userUtils";
import { userData } from "../../src/features/User/userDataTypes";
describe("userUtils", () => {
describe("getUserDisplayName", () => {
it("returns full name when user has name and family_name", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "John",
name: "John",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("John Doe");
});
it("returns email when user does not have name and family_name", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
it("returns email when user has only email", () => {
const user: userData = {
sub: "test",
email: "user@example.com",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("user@example.com");
});
it("returns empty string when user is null", () => {
expect(getUserDisplayName(null)).toBe("");
});
it("returns empty string when user is undefined", () => {
expect(getUserDisplayName(undefined)).toBe("");
});
it("returns empty string when user has no name, family_name, or email", () => {
const user: userData = {
sub: "test",
email: "",
family_name: "",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("");
});
it("handles user with only name (no family_name)", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "",
given_name: "John",
name: "John",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
it("handles user with only family_name (no name)", () => {
const user: userData = {
sub: "test",
email: "test@test.com",
family_name: "Doe",
given_name: "",
name: "",
sid: "mockSid",
};
expect(getUserDisplayName(user)).toBe("test@test.com");
});
});
});
+15 -1
View File
@@ -1,4 +1,4 @@
import { useRef, useState } from "react"; import { useRef, useState, useEffect } from "react";
import { Menubar, MenubarProps } from "../Menubar/Menubar"; import { Menubar, MenubarProps } from "../Menubar/Menubar";
import CalendarApp from "./Calendar"; import CalendarApp from "./Calendar";
import { useAppDispatch } from "../../app/hooks"; import { useAppDispatch } from "../../app/hooks";
@@ -51,6 +51,20 @@ export default function CalendarLayout() {
setCurrentView(view); setCurrentView(view);
}; };
// Hide topbar navigation elements when in settings view (same as fullscreen dialog mode)
useEffect(() => {
if (view === "settings") {
document.body.classList.add("fullscreen-view");
} else {
document.body.classList.remove("fullscreen-view");
}
// Cleanup on unmount
return () => {
document.body.classList.remove("fullscreen-view");
};
}, [view]);
const menubarProps: MenubarProps = { const menubarProps: MenubarProps = {
calendarRef, calendarRef,
onRefresh: handleRefresh, onRefresh: handleRefresh,
+2 -2
View File
@@ -149,9 +149,9 @@ function ResponsiveDialog({
React.useEffect(() => { React.useEffect(() => {
if (isExpanded) { if (isExpanded) {
document.body.classList.add("dialog-expanded"); document.body.classList.add("fullscreen-view");
} else { } else {
document.body.classList.remove("dialog-expanded"); document.body.classList.remove("fullscreen-view");
} }
}, [isExpanded]); }, [isExpanded]);
+4 -4
View File
@@ -92,8 +92,8 @@
justify-content flex-end justify-content flex-end
align-items center align-items center
body.dialog-expanded .navigation-controls, body.fullscreen-view .navigation-controls,
body.dialog-expanded .current-date-time, body.fullscreen-view .current-date-time,
body.dialog-expanded .refresh-button, body.fullscreen-view .refresh-button,
body.dialog-expanded .select-display body.fullscreen-view .select-display
display none display none
+81 -35
View File
@@ -4,6 +4,8 @@ import AppsIcon from "@mui/icons-material/Apps";
import RefreshIcon from "@mui/icons-material/Refresh"; import RefreshIcon from "@mui/icons-material/Refresh";
import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft";
import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import ChevronRightIcon from "@mui/icons-material/ChevronRight";
import SettingsIcon from "@mui/icons-material/Settings";
import LogoutIcon from "@mui/icons-material/Logout";
import "./Menubar.styl"; import "./Menubar.styl";
import { useAppDispatch, useAppSelector } from "../../app/hooks"; import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { stringToColor } from "../Event/utils/eventUtils"; import { stringToColor } from "../Event/utils/eventUtils";
@@ -11,17 +13,21 @@ import {
Avatar, Avatar,
IconButton, IconButton,
Popover, Popover,
Menu,
MenuItem,
ButtonGroup, ButtonGroup,
Button, Button,
Select, Select,
MenuItem,
FormControl, FormControl,
Typography, Typography,
Box,
Divider,
} from "@mui/material"; } from "@mui/material";
import { push } from "redux-first-history"; import { push } from "redux-first-history";
import { CalendarApi } from "@fullcalendar/core"; import { CalendarApi } from "@fullcalendar/core";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n"; import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import { setLanguage } from "../../features/Settings/SettingsSlice"; import { setView } from "../../features/Settings/SettingsSlice";
import { getUserDisplayName } from "../../utils/userUtils";
import { format } from "date-fns"; import { format } from "date-fns";
import { import {
enGB, enGB,
@@ -58,7 +64,9 @@ export function Menubar({
const user = useAppSelector((state) => state.user.userData); const user = useAppSelector((state) => state.user.userData);
const applist: AppIconProps[] = (window as any).appList ?? []; const applist: AppIconProps[] = (window as any).appList ?? [];
const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null);
const [langAnchorEl, setLangAnchorEl] = useState<null | HTMLElement>(null); const [userMenuAnchorEl, setUserMenuAnchorEl] = useState<null | HTMLElement>(
null
);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
useEffect(() => { useEffect(() => {
@@ -117,24 +125,30 @@ export function Menubar({
}; };
const open = Boolean(anchorEl); const open = Boolean(anchorEl);
const langOpen = Boolean(langAnchorEl); const userMenuOpen = Boolean(userMenuAnchorEl);
const handleLangClick = (event: React.MouseEvent<HTMLElement>) => { const handleUserMenuClick = (event: React.MouseEvent<HTMLElement>) => {
setLangAnchorEl(event.currentTarget); setUserMenuAnchorEl(event.currentTarget);
};
const handleUserMenuClose = () => {
setUserMenuAnchorEl(null);
};
const handleSettingsClick = () => {
dispatch(setView("settings"));
handleUserMenuClose();
};
const handleLogoutClick = () => {
// TODO: Implement logout functionality
handleUserMenuClose();
}; };
const handleLangClose = () => setLangAnchorEl(null);
const dateLabel = format(currentDate, "MMMM yyyy", { const dateLabel = format(currentDate, "MMMM yyyy", {
locale: dateLocales[lang as keyof typeof dateLocales] || enGB, locale: dateLocales[lang as keyof typeof dateLocales] || enGB,
}); });
const availableLangs = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
{ code: "ru", label: "Русский" },
{ code: "vi", label: "Tiếng Việt" },
];
return ( return (
<> <>
<header className="menubar"> <header className="menubar">
@@ -225,7 +239,7 @@ export function Menubar({
</div> </div>
<div className="menu-items"> <div className="menu-items">
<IconButton onClick={handleLangClick}> <IconButton onClick={handleUserMenuClick}>
<Avatar <Avatar
style={{ style={{
backgroundColor: stringToColor( backgroundColor: stringToColor(
@@ -267,10 +281,10 @@ export function Menubar({
</div> </div>
</Popover> </Popover>
<Popover <Menu
open={langOpen} open={userMenuOpen}
anchorEl={langAnchorEl} anchorEl={userMenuAnchorEl}
onClose={handleLangClose} onClose={handleUserMenuClose}
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "right", horizontal: "right",
@@ -279,25 +293,57 @@ export function Menubar({
vertical: "top", vertical: "top",
horizontal: "right", horizontal: "right",
}} }}
PaperProps={{
sx: {
minWidth: 280,
mt: 1,
padding: "0 !important",
borderRadius: "16px",
},
}}
> >
<FormControl size="small" style={{ minWidth: 100, marginRight: 8 }}> <Box
<Select sx={{
value={lang} display: "flex",
onChange={(e) => { flexDirection: "column",
dispatch(setLanguage(e.target.value)); alignItems: "center",
handleLangClose(); padding: "24px",
}}
>
<Avatar
style={{
backgroundColor: stringToColor(
user && user.family_name
? user.family_name
: user && user.email
? user.email
: ""
),
marginBottom: "8px",
}} }}
variant="outlined" sizes="large"
aria-label={t("menubar.languageSelector")}
> >
{availableLangs.map(({ code, label }) => ( {user?.name && user?.family_name
<MenuItem key={code} value={code}> ? `${user.name[0]}${user.family_name[0]}`
{label} : (user?.email?.[0] ?? "")}
</MenuItem> </Avatar>
))} <Typography variant="body1" sx={{ fontWeight: 500 }}>
</Select> {getUserDisplayName(user)}
</FormControl> </Typography>
</Popover> <Typography variant="body2" color="text.secondary">
{user?.email}
</Typography>
</Box>
<MenuItem onClick={handleSettingsClick} sx={{ py: 1.5 }}>
<SettingsIcon sx={{ mr: 2 }} />
{t("menubar.settings") || "Settings"}
</MenuItem>
{/* <Divider />
<MenuItem onClick={handleLogoutClick} sx={{ py: 1.5 }}>
<LogoutIcon sx={{ mr: 2 }} />
{t("menubar.logout") || "Logout"}
</MenuItem> */}
</Menu>
</> </>
); );
} }
+105
View File
@@ -0,0 +1,105 @@
.main-layout.settings-layout
display flex
flex-direction row
height calc(100vh - 90px)
padding 0 10px 10px 0;
.settings-sidebar
width 350px
height 100%
overflow-y auto
overflow-x hidden
scrollbar-width thin
scrollbar-color transparent transparent
.settings-sidebar::-webkit-scrollbar
width 8px
.settings-sidebar::-webkit-scrollbar-track
background transparent
.settings-sidebar::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.3)
border-radius 4px
opacity 0
transition opacity 0.3s ease
.settings-sidebar:hover::-webkit-scrollbar-thumb
background-color rgba(0, 0, 0, 0.5)
opacity 1
.settings-sidebar:hover
scrollbar-color rgba(0, 0, 0, 0.5) transparent
.settings-nav-item
position relative
padding 12px 24px
transition background-color 0.2s ease
cursor pointer
&::before
content ""
position absolute
left 0
top 0
width 0
height 100%
background-color #F67E35
transition width 0.2s ease
&:hover
background-color #f9fafb
&::before
width 4px
.MuiListItemIcon-root
color #F67E35
&.active
&::before
width 4px
.MuiListItemIcon-root
color #F67E35
.settings-content
flex-grow 1
display flex
flex-direction column
height 100%
overflow-y auto
border-radius 16px
background #fff
.settings-content-header
display flex
align-items center
padding 16px 24px
gap 16px
.back-button
.MuiSvgIcon-root
font-size 28px
.settings-content-body
padding 24px
flex-grow 1
.settings-tab-content
text-align left
.settings-content-tabs
.MuiTab-root
color #424244E5
text-transform none
font-weight 600
font-size 24px
.MuiTab-root.Mui-selected
color #424244E5
.MuiTabs-indicator
background-color #F67E35
+155 -2
View File
@@ -1,9 +1,162 @@
import React from "react"; import React, { useState } from "react";
import {
Box,
List,
ListItem,
ListItemIcon,
ListItemText,
Tabs,
Tab,
IconButton,
FormControl,
Select,
MenuItem,
Typography,
} from "@mui/material";
import ArrowBackIcon from "@mui/icons-material/ArrowBack";
import SettingsIcon from "@mui/icons-material/Settings";
import SyncIcon from "@mui/icons-material/Sync";
import { useAppDispatch, useAppSelector } from "../../app/hooks";
import { setView, setLanguage } from "./SettingsSlice";
import { AVAILABLE_LANGUAGES } from "./constants";
import { useI18n } from "cozy-ui/transpiled/react/providers/I18n";
import "./SettingsPage.styl";
type SidebarNavItem = "settings" | "sync";
type SettingsSubTab = "settings" | "notifications";
export default function SettingsPage() { export default function SettingsPage() {
const dispatch = useAppDispatch();
const { t, lang } = useI18n();
const [activeNavItem, setActiveNavItem] =
useState<SidebarNavItem>("settings");
const [activeSettingsSubTab, setActiveSettingsSubTab] =
useState<SettingsSubTab>("settings");
const handleBackClick = () => {
dispatch(setView("calendar"));
};
const handleNavItemClick = (item: SidebarNavItem) => {
setActiveNavItem(item);
if (item === "settings") {
setActiveSettingsSubTab("settings");
}
};
const handleSettingsSubTabChange = (
_event: React.SyntheticEvent,
newValue: SettingsSubTab
) => {
setActiveSettingsSubTab(newValue);
};
const handleLanguageChange = (event: any) => {
dispatch(setLanguage(event.target.value));
};
return ( return (
<main className="main-layout settings-layout"> <main className="main-layout settings-layout">
<h1>Settings Page</h1> <Box className="settings-sidebar">
<List>
<ListItem
className={`settings-nav-item ${activeNavItem === "settings" ? "active" : ""}`}
onClick={() => handleNavItemClick("settings")}
sx={{ cursor: "pointer" }}
>
<ListItemIcon>
<SettingsIcon />
</ListItemIcon>
<ListItemText primary={t("settings.title") || "Settings"} />
</ListItem>
{/* <ListItem
className={`settings-nav-item ${activeNavItem === "sync" ? "active" : ""}`}
onClick={() => handleNavItemClick("sync")}
sx={{ cursor: "pointer" }}
>
<ListItemIcon>
<SyncIcon />
</ListItemIcon>
<ListItemText primary={t("settings.sync") || "Sync"} />
</ListItem> */}
</List>
</Box>
<Box className="settings-content">
<Box className="settings-content-header">
<IconButton
onClick={handleBackClick}
aria-label={t("settings.back") || "Back to calendar"}
className="back-button"
>
<ArrowBackIcon />
</IconButton>
{activeNavItem === "settings" && (
<Tabs
value={activeSettingsSubTab}
onChange={handleSettingsSubTabChange}
className="settings-content-tabs"
>
<Tab value="settings" label={t("settings.title") || "Settings"} />
<Tab
value="notifications"
label={t("settings.notifications") || "Notifications"}
/>
</Tabs>
)}
</Box>
<Box className="settings-content-body">
{activeNavItem === "settings" && (
<>
{activeSettingsSubTab === "settings" && (
<Box className="settings-tab-content">
<Typography variant="h6" sx={{ mb: 1 }}>
{t("settings.language") || "Language"}
</Typography>
<Typography
variant="body2"
color="text.secondary"
sx={{ mb: 3 }}
>
{t("settings.languageDescription") ||
"This will be the language used in your Twake Calendar"}
</Typography>
<FormControl size="small" sx={{ minWidth: 500 }}>
<Select
value={lang}
onChange={handleLanguageChange}
variant="outlined"
aria-label={
t("settings.languageSelector") || "Language selector"
}
>
{AVAILABLE_LANGUAGES.map(({ code, label }) => (
<MenuItem key={code} value={code}>
{label}
</MenuItem>
))}
</Select>
</FormControl>
</Box>
)}
{activeSettingsSubTab === "notifications" && (
<Box className="settings-tab-content">
<Typography variant="body1" color="text.secondary">
{t("settings.notifications.empty") ||
"Notifications settings coming soon"}
</Typography>
</Box>
)}
</>
)}
{activeNavItem === "sync" && (
<Box className="settings-tab-content">
<Typography variant="body1" color="text.secondary">
{t("settings.sync.empty") || "Sync settings coming soon"}
</Typography>
</Box>
)}
</Box>
</Box>
</main> </main>
); );
} }
+6
View File
@@ -0,0 +1,6 @@
export const AVAILABLE_LANGUAGES = [
{ code: "en", label: "English" },
{ code: "fr", label: "Français" },
{ code: "ru", label: "Русский" },
{ code: "vi", label: "Tiếng Việt" },
] as const;
+14 -1
View File
@@ -185,7 +185,20 @@
}, },
"apps": "Apps", "apps": "Apps",
"userProfile": "User profile", "userProfile": "User profile",
"logoAlt": "Calendar" "logoAlt": "Calendar",
"settings": "Settings",
"logout": "Logout"
},
"settings": {
"title": "Settings",
"notifications": "Notifications",
"sync": "Sync",
"language": "Language",
"languageDescription": "This will be the language used in your Twake Calendar",
"languageSelector": "Language selector",
"notifications.empty": "Notifications settings coming soon",
"sync.empty": "Sync settings coming soon",
"back": "Back to calendar"
}, },
"eventPreview": { "eventPreview": {
"emailAttendees": "Email attendees", "emailAttendees": "Email attendees",
+14 -1
View File
@@ -185,7 +185,20 @@
}, },
"apps": "Applications", "apps": "Applications",
"userProfile": "Profil utilisateur", "userProfile": "Profil utilisateur",
"logoAlt": "Calendrier" "logoAlt": "Calendrier",
"settings": "Paramètres",
"logout": "Déconnexion"
},
"settings": {
"title": "Paramètres",
"notifications": "Notifications",
"sync": "Synchronisation",
"language": "Langue",
"languageDescription": "Ce sera la langue utilisée dans votre Twake Calendar",
"languageSelector": "Sélecteur de langue",
"notifications.empty": "Paramètres de notifications à venir",
"sync.empty": "Paramètres de synchronisation à venir",
"back": "Retour au calendrier"
}, },
"eventPreview": { "eventPreview": {
"emailAttendees": "Envoyer un e-mail aux participants", "emailAttendees": "Envoyer un e-mail aux participants",
+14 -1
View File
@@ -185,7 +185,20 @@
}, },
"apps": "Приложения", "apps": "Приложения",
"userProfile": "Профиль", "userProfile": "Профиль",
"logoAlt": "Календарь" "logoAlt": "Календарь",
"settings": "Настройки",
"logout": "Выйти"
},
"settings": {
"title": "Настройки",
"notifications": "Уведомления",
"sync": "Синхронизация",
"language": "Язык",
"languageDescription": "Это будет язык, используемый в вашем Twake Calendar",
"languageSelector": "Выбор языка",
"notifications.empty": "Настройки уведомлений скоро появятся",
"sync.empty": "Настройки синхронизации скоро появятся",
"back": "Вернуться к календарю"
}, },
"eventPreview": { "eventPreview": {
"emailAttendees": "Написать участникам", "emailAttendees": "Написать участникам",
+14 -1
View File
@@ -185,7 +185,20 @@
}, },
"apps": "Ứng dụng", "apps": "Ứng dụng",
"userProfile": "Hồ sơ người dùng", "userProfile": "Hồ sơ người dùng",
"logoAlt": "Lịch" "logoAlt": "Lịch",
"settings": "Cài đặt",
"logout": "Đăng xuất"
},
"settings": {
"title": "Cài đặt",
"notifications": "Thông báo",
"sync": "Đồng bộ",
"language": "Ngôn ngữ",
"languageDescription": "Đây sẽ là ngôn ngữ được sử dụng trong Twake Calendar của bạn",
"languageSelector": "Chọn ngôn ngữ",
"notifications.empty": "Cài đặt thông báo sắp có",
"sync.empty": "Cài đặt đồng bộ sắp có",
"back": "Quay lại lịch"
}, },
"eventPreview": { "eventPreview": {
"emailAttendees": "Gửi email cho người tham gia", "emailAttendees": "Gửi email cho người tham gia",
+19
View File
@@ -0,0 +1,19 @@
import { userData } from "../features/User/userDataTypes";
/**
* Get user display name for UI display
* Returns full name if available, otherwise email, or empty string as fallback
* @param user - User data object
* @returns Display name string
*/
export function getUserDisplayName(user: userData | null | undefined): string {
if (!user) {
return "";
}
if (user.name && user.family_name) {
return `${user.name} ${user.family_name}`;
}
return user.email || "";
}