* 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:
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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 || "";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user