feat: Update Event Preview Modal UI with custom MUI theme (#240)
- Add custom MUI theme - Update ResponsiveDialog with actionsBorderTop prop for modal footer border - Redesign EventDisplayPreview modal with improved typography and spacing - Replace ButtonGroup with individual RSVP buttons (Accept/Maybe/Decline) - Add pill-shaped buttons for RSVP actions - Ensure backward compatibility with existing modals
This commit is contained in:
@@ -62,8 +62,9 @@ const preloadedState = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("EventDuplication", () => {
|
describe("EventDuplication", () => {
|
||||||
it("opens EventPopover when button clicked", () => {
|
it("calls onOpenDuplicate when button clicked", () => {
|
||||||
const handleClose = jest.fn();
|
const handleClose = jest.fn();
|
||||||
|
const onOpenDuplicate = jest.fn();
|
||||||
renderWithProviders(
|
renderWithProviders(
|
||||||
<EventDuplication
|
<EventDuplication
|
||||||
event={
|
event={
|
||||||
@@ -71,36 +72,14 @@ describe("EventDuplication", () => {
|
|||||||
.event1
|
.event1
|
||||||
}
|
}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
|
onOpenDuplicate={onOpenDuplicate}
|
||||||
/>,
|
/>,
|
||||||
preloadedState
|
preloadedState
|
||||||
);
|
);
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i }));
|
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i }));
|
||||||
|
|
||||||
expect(screen.getAllByText(/Duplicate Event/i)[1]).toBeInTheDocument();
|
expect(onOpenDuplicate).toHaveBeenCalled();
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onClose when closing the popover", () => {
|
|
||||||
const handleClose = jest.fn();
|
|
||||||
renderWithProviders(
|
|
||||||
<EventDuplication
|
|
||||||
event={
|
|
||||||
preloadedState.calendars.list["667037022b752d0026472254/cal1"].events
|
|
||||||
.event1
|
|
||||||
}
|
|
||||||
onClose={handleClose}
|
|
||||||
/>,
|
|
||||||
preloadedState
|
|
||||||
);
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i }));
|
|
||||||
|
|
||||||
// Cancel button only appears in expanded mode
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /More options/i }));
|
|
||||||
|
|
||||||
fireEvent.click(screen.getByRole("button", { name: /Cancel/i }));
|
|
||||||
|
|
||||||
expect(handleClose).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
+14
-10
@@ -8,18 +8,22 @@ import { Loading } from "./components/Loading/Loading";
|
|||||||
import HandleLogin from "./features/User/HandleLogin";
|
import HandleLogin from "./features/User/HandleLogin";
|
||||||
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||||
import { Error } from "./components/Error/Error";
|
import { Error } from "./components/Error/Error";
|
||||||
|
import { CustomThemeProvider } from "./theme/ThemeProvider";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Suspense fallback={<Loading />}>
|
<CustomThemeProvider>
|
||||||
<Router history={history}>
|
<Suspense fallback={<Loading />}>
|
||||||
<Routes>
|
<Router history={history}>
|
||||||
<Route path="/" element={<HandleLogin />} />
|
<Routes>
|
||||||
<Route path="/calendar" element={<CalendarLayout />} />
|
<Route path="/" element={<HandleLogin />} />
|
||||||
<Route path="/callback" element={<CallbackResume />} />
|
<Route path="/calendar" element={<CalendarLayout />} />
|
||||||
<Route path="/error" element={<Error />} />
|
<Route path="/callback" element={<CallbackResume />} />
|
||||||
</Routes>
|
<Route path="/error" element={<Error />} />
|
||||||
</Router>
|
</Routes>
|
||||||
</Suspense>
|
</Router>
|
||||||
|
</Suspense>
|
||||||
|
</CustomThemeProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -66,17 +66,19 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
|||||||
}}
|
}}
|
||||||
slotProps={{
|
slotProps={{
|
||||||
paper: {
|
paper: {
|
||||||
sx: { width: 280, maxHeight: 400, overflow: "auto", p: 1 },
|
sx: { width: 280, maxHeight: 400, overflow: "hidden", p: 0 },
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<TimezoneAutocomplete
|
<TimezoneAutocomplete
|
||||||
|
size="medium"
|
||||||
value={effectiveTimezone}
|
value={effectiveTimezone}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
zones={timezoneList.zones}
|
zones={timezoneList.zones}
|
||||||
getTimezoneOffset={getTimezoneOffset}
|
getTimezoneOffset={getTimezoneOffset}
|
||||||
autoFocus={true}
|
autoFocus={true}
|
||||||
inputFontSize="10px"
|
showIcon={true}
|
||||||
|
inputFontSize="14px"
|
||||||
inputPadding="2px 4px"
|
inputPadding="2px 4px"
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
disableClearable={true}
|
disableClearable={true}
|
||||||
|
|||||||
@@ -78,6 +78,14 @@ interface ResponsiveDialogProps
|
|||||||
dividers?: boolean;
|
dividers?: boolean;
|
||||||
/** Whether to show header action icons (expand/close) in normal mode (default: true) */
|
/** Whether to show header action icons (expand/close) in normal mode (default: true) */
|
||||||
showHeaderActions?: boolean;
|
showHeaderActions?: boolean;
|
||||||
|
/** Whether to add border-top to DialogActions */
|
||||||
|
actionsBorderTop?: boolean;
|
||||||
|
/** Justify content alignment for DialogActions */
|
||||||
|
actionsJustifyContent?:
|
||||||
|
| "flex-start"
|
||||||
|
| "center"
|
||||||
|
| "flex-end"
|
||||||
|
| "space-between";
|
||||||
}
|
}
|
||||||
|
|
||||||
function ResponsiveDialog({
|
function ResponsiveDialog({
|
||||||
@@ -99,6 +107,8 @@ function ResponsiveDialog({
|
|||||||
dialogTitleProps,
|
dialogTitleProps,
|
||||||
dividers = false,
|
dividers = false,
|
||||||
showHeaderActions = true,
|
showHeaderActions = true,
|
||||||
|
actionsBorderTop = false,
|
||||||
|
actionsJustifyContent = "flex-end",
|
||||||
sx,
|
sx,
|
||||||
...otherDialogProps
|
...otherDialogProps
|
||||||
}: ResponsiveDialogProps) {
|
}: ResponsiveDialogProps) {
|
||||||
@@ -199,7 +209,20 @@ function ResponsiveDialog({
|
|||||||
<Stack spacing={currentSpacing}>{children}</Stack>
|
<Stack spacing={currentSpacing}>{children}</Stack>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
{actions && <DialogActions>{actions}</DialogActions>}
|
{actions && (
|
||||||
|
<DialogActions
|
||||||
|
sx={{
|
||||||
|
borderTop: actionsBorderTop
|
||||||
|
? (theme) => `1px solid ${theme.palette.divider}`
|
||||||
|
: undefined,
|
||||||
|
justifyContent: actionsJustifyContent,
|
||||||
|
paddingTop: "18px",
|
||||||
|
paddingBottom: "18px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{actions}
|
||||||
|
</DialogActions>
|
||||||
|
)}
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,51 +1,22 @@
|
|||||||
import { IconButton, MenuItem } from "@mui/material";
|
import { MenuItem } from "@mui/material";
|
||||||
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
import { CalendarEvent } from "../../features/Events/EventsTypes";
|
||||||
import AddToPhotosIcon from "@mui/icons-material/AddToPhotos";
|
|
||||||
import { useRef, useState } from "react";
|
|
||||||
import EventPopover from "../../features/Events/EventModal";
|
|
||||||
import { CalendarApi, DateSelectArg } from "@fullcalendar/core";
|
|
||||||
|
|
||||||
export default function EventDuplication({
|
export default function EventDuplication({
|
||||||
onClose,
|
onClose,
|
||||||
event,
|
event,
|
||||||
|
onOpenDuplicate,
|
||||||
}: {
|
}: {
|
||||||
onClose: Function;
|
onClose: Function;
|
||||||
event: CalendarEvent;
|
event: CalendarEvent;
|
||||||
|
onOpenDuplicate?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const [openModal, setOpenModal] = useState(false);
|
|
||||||
const [selectedRange, setSelectedRange] = useState<DateSelectArg | null>({
|
|
||||||
start: new Date(event.start),
|
|
||||||
startStr: event.start,
|
|
||||||
end: new Date(event.end ?? ""),
|
|
||||||
endStr: event.end ?? "",
|
|
||||||
allDay: event.allday ?? false,
|
|
||||||
} as DateSelectArg);
|
|
||||||
const calendarRef = useRef<CalendarApi | null>(null);
|
|
||||||
|
|
||||||
const handleClosePopover = () => {
|
|
||||||
calendarRef.current?.unselect();
|
|
||||||
setSelectedRange(null);
|
|
||||||
setOpenModal(!openModal);
|
|
||||||
onClose({}, "backdropClick");
|
|
||||||
};
|
|
||||||
return (
|
return (
|
||||||
<>
|
<MenuItem
|
||||||
<MenuItem
|
onClick={() => {
|
||||||
onClick={() => {
|
onOpenDuplicate?.();
|
||||||
setOpenModal(true);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
Duplicate event
|
||||||
Duplicate event
|
</MenuItem>
|
||||||
</MenuItem>
|
|
||||||
<EventPopover
|
|
||||||
anchorEl={null}
|
|
||||||
open={openModal}
|
|
||||||
selectedRange={selectedRange}
|
|
||||||
setSelectedRange={setSelectedRange}
|
|
||||||
calendarRef={calendarRef}
|
|
||||||
onClose={handleClosePopover}
|
|
||||||
event={event}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -77,12 +77,14 @@ export function InfoRow({
|
|||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
variant="body2"
|
||||||
color={error ? "error" : "textPrimary"}
|
color={error ? "error" : "textPrimary"}
|
||||||
style={{
|
sx={{
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
whiteSpace: "pre-line",
|
whiteSpace: "pre-line",
|
||||||
maxHeight: "33vh",
|
maxHeight: "33vh",
|
||||||
overflowY: "auto",
|
overflowY: "auto",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
|
fontSize: "16px",
|
||||||
|
fontFamily: "'Inter', sans-serif",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{data ? (
|
{data ? (
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ export function renderAttendeeBadge(
|
|||||||
>
|
>
|
||||||
<Badge
|
<Badge
|
||||||
overlap="circular"
|
overlap="circular"
|
||||||
|
sx={{ marginRight: 2 }}
|
||||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||||
badgeContent={
|
badgeContent={
|
||||||
classIcon && (
|
classIcon && (
|
||||||
@@ -65,7 +66,6 @@ export function renderAttendeeBadge(
|
|||||||
</Badge>
|
</Badge>
|
||||||
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
|
||||||
noWrap
|
noWrap
|
||||||
style={{
|
style={{
|
||||||
maxWidth: "180px",
|
maxWidth: "180px",
|
||||||
@@ -76,11 +76,7 @@ export function renderAttendeeBadge(
|
|||||||
{a.cn || a.cal_address}
|
{a.cn || a.cal_address}
|
||||||
</Typography>
|
</Typography>
|
||||||
{isOrganizer && (
|
{isOrganizer && (
|
||||||
<Typography
|
<Typography variant="caption" color="text.secondary">
|
||||||
variant="caption"
|
|
||||||
color="text.secondary"
|
|
||||||
style={{ fontStyle: "italic" }}
|
|
||||||
>
|
|
||||||
Organizer
|
Organizer
|
||||||
</Typography>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -12,13 +12,13 @@ import SubjectIcon from "@mui/icons-material/Subject";
|
|||||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||||
import RepeatIcon from "@mui/icons-material/Repeat";
|
import RepeatIcon from "@mui/icons-material/Repeat";
|
||||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||||
|
import EventPopover from "./EventModal";
|
||||||
|
import { DateSelectArg } from "@fullcalendar/core";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
|
||||||
Chip,
|
Chip,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Divider,
|
|
||||||
IconButton,
|
IconButton,
|
||||||
Menu,
|
Menu,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
@@ -77,6 +77,7 @@ export default function EventPreviewModal({
|
|||||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||||
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
||||||
|
const [openDuplicateModal, setOpenDuplicateModal] = useState(false);
|
||||||
const [hidePreview, setHidePreview] = useState(false);
|
const [hidePreview, setHidePreview] = useState(false);
|
||||||
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||||
null
|
null
|
||||||
@@ -136,6 +137,8 @@ export default function EventPreviewModal({
|
|||||||
open={open && !hidePreview}
|
open={open && !hidePreview}
|
||||||
onClose={() => onClose({}, "backdropClick")}
|
onClose={() => onClose({}, "backdropClick")}
|
||||||
showHeaderActions={false}
|
showHeaderActions={false}
|
||||||
|
actionsBorderTop={true}
|
||||||
|
actionsJustifyContent="center"
|
||||||
style={{ overflow: "auto" }}
|
style={{ overflow: "auto" }}
|
||||||
title={
|
title={
|
||||||
event.title && (
|
event.title && (
|
||||||
@@ -210,7 +213,15 @@ export default function EventPreviewModal({
|
|||||||
Email attendees
|
Email attendees
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
<EventDuplication event={event} onClose={onClose} />
|
<EventDuplication
|
||||||
|
event={event}
|
||||||
|
onClose={() => setToggleActionMenu(null)}
|
||||||
|
onOpenDuplicate={() => {
|
||||||
|
setToggleActionMenu(null);
|
||||||
|
setHidePreview(true);
|
||||||
|
setOpenDuplicateModal(true);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
{user.userData.email === event.organizer?.cal_address && (
|
{user.userData.email === event.organizer?.cal_address && (
|
||||||
<MenuItem
|
<MenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
@@ -270,10 +281,12 @@ export default function EventPreviewModal({
|
|||||||
<LockOutlineIcon />
|
<LockOutlineIcon />
|
||||||
))}
|
))}
|
||||||
<Typography
|
<Typography
|
||||||
variant="inherit"
|
variant="h5"
|
||||||
fontWeight="bold"
|
sx={{
|
||||||
style={{
|
fontSize: "24px",
|
||||||
|
fontWeight: 600,
|
||||||
wordBreak: "break-word",
|
wordBreak: "break-word",
|
||||||
|
fontFamily: "Inter, sans-serif",
|
||||||
}}
|
}}
|
||||||
gutterBottom
|
gutterBottom
|
||||||
>
|
>
|
||||||
@@ -293,7 +306,7 @@ export default function EventPreviewModal({
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
<Typography color="text.secondaryContainer" gutterBottom>
|
||||||
{formatDate(event.start, event.allday)}
|
{formatDate(event.start, event.allday)}
|
||||||
{event.end &&
|
{event.end &&
|
||||||
formatEnd(event.start, event.end, event.allday) &&
|
formatEnd(event.start, event.end, event.allday) &&
|
||||||
@@ -305,15 +318,22 @@ export default function EventPreviewModal({
|
|||||||
actions={
|
actions={
|
||||||
<>
|
<>
|
||||||
{currentUserAttendee && (
|
{currentUserAttendee && (
|
||||||
<Box style={{ display: "flex", flexDirection: "row" }}>
|
<>
|
||||||
<Typography variant="body2">Attending?</Typography>
|
<Typography sx={{ marginRight: 2 }}>Attending?</Typography>
|
||||||
<ButtonGroup size="small" fullWidth>
|
<Box display="flex" gap="15px" alignItems="center">
|
||||||
<Button
|
<Button
|
||||||
|
variant={
|
||||||
|
currentUserAttendee?.partstat === "ACCEPTED"
|
||||||
|
? "contained"
|
||||||
|
: "outlined"
|
||||||
|
}
|
||||||
color={
|
color={
|
||||||
currentUserAttendee?.partstat === "ACCEPTED"
|
currentUserAttendee?.partstat === "ACCEPTED"
|
||||||
? "success"
|
? "success"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
|
size="large"
|
||||||
|
sx={{ borderRadius: "50px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isRecurring) {
|
if (isRecurring) {
|
||||||
setAfterChoiceFunc(
|
setAfterChoiceFunc(
|
||||||
@@ -346,11 +366,18 @@ export default function EventPreviewModal({
|
|||||||
Accept
|
Accept
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant={
|
||||||
|
currentUserAttendee?.partstat === "TENTATIVE"
|
||||||
|
? "contained"
|
||||||
|
: "outlined"
|
||||||
|
}
|
||||||
color={
|
color={
|
||||||
currentUserAttendee?.partstat === "TENTATIVE"
|
currentUserAttendee?.partstat === "TENTATIVE"
|
||||||
? "warning"
|
? "warning"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
|
size="large"
|
||||||
|
sx={{ borderRadius: "50px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isRecurring) {
|
if (isRecurring) {
|
||||||
setAfterChoiceFunc(
|
setAfterChoiceFunc(
|
||||||
@@ -382,11 +409,18 @@ export default function EventPreviewModal({
|
|||||||
Maybe
|
Maybe
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
variant={
|
||||||
|
currentUserAttendee?.partstat === "DECLINED"
|
||||||
|
? "contained"
|
||||||
|
: "outlined"
|
||||||
|
}
|
||||||
color={
|
color={
|
||||||
currentUserAttendee?.partstat === "DECLINED"
|
currentUserAttendee?.partstat === "DECLINED"
|
||||||
? "error"
|
? "error"
|
||||||
: "primary"
|
: "primary"
|
||||||
}
|
}
|
||||||
|
size="large"
|
||||||
|
sx={{ borderRadius: "50px" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (isRecurring) {
|
if (isRecurring) {
|
||||||
setAfterChoiceFunc(
|
setAfterChoiceFunc(
|
||||||
@@ -417,8 +451,8 @@ export default function EventPreviewModal({
|
|||||||
>
|
>
|
||||||
Decline
|
Decline
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</Box>
|
||||||
</Box>
|
</>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
@@ -428,7 +462,11 @@ export default function EventPreviewModal({
|
|||||||
{/* Video */}
|
{/* Video */}
|
||||||
{event.x_openpass_videoconference && (
|
{event.x_openpass_videoconference && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<VideocamIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
content={
|
content={
|
||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
@@ -445,7 +483,11 @@ export default function EventPreviewModal({
|
|||||||
{attendees?.length > 0 && (
|
{attendees?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<PeopleAltOutlinedIcon />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<PeopleAltOutlinedIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
content={
|
content={
|
||||||
<Box
|
<Box
|
||||||
style={{
|
style={{
|
||||||
@@ -454,9 +496,11 @@ export default function EventPreviewModal({
|
|||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box>
|
<Box sx={{ marginRight: 2 }}>
|
||||||
<Typography>{attendees.length} guests</Typography>
|
<Typography>{attendees.length} guests</Typography>
|
||||||
<Typography>
|
<Typography
|
||||||
|
sx={{ fontSize: "13px", color: "text.secondary" }}
|
||||||
|
>
|
||||||
{
|
{
|
||||||
attendees.filter((a) => a.partstat === "ACCEPTED")
|
attendees.filter((a) => a.partstat === "ACCEPTED")
|
||||||
.length
|
.length
|
||||||
@@ -488,9 +532,13 @@ export default function EventPreviewModal({
|
|||||||
</AvatarGroup>
|
</AvatarGroup>
|
||||||
)}
|
)}
|
||||||
<Typography
|
<Typography
|
||||||
variant="body2"
|
sx={{
|
||||||
color="primary"
|
cursor: "pointer",
|
||||||
style={{ cursor: "pointer", marginTop: 0.5 }}
|
marginLeft: 2,
|
||||||
|
fontSize: "14px",
|
||||||
|
color: "text.secondary",
|
||||||
|
alignSelf: "center",
|
||||||
|
}}
|
||||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||||
>
|
>
|
||||||
{showAllAttendees ? "Show less" : `Show more `}
|
{showAllAttendees ? "Show less" : `Show more `}
|
||||||
@@ -510,25 +558,44 @@ export default function EventPreviewModal({
|
|||||||
{/* Location */}
|
{/* Location */}
|
||||||
{event.location && (
|
{event.location && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<LocationOnOutlinedIcon />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<LocationOnOutlinedIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
text={event.location}
|
text={event.location}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Description */}
|
{/* Description */}
|
||||||
{event.description && (
|
{event.description && (
|
||||||
<InfoRow icon={<SubjectIcon />} text={event.description} />
|
<InfoRow
|
||||||
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<SubjectIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
|
text={event.description}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{/* ALARM */}
|
{/* ALARM */}
|
||||||
{event.alarm && (
|
{event.alarm && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<NotificationsNoneIcon />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<NotificationsNoneIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
|
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{/* Repetition */}
|
{/* Repetition */}
|
||||||
{event.repetition && (
|
{event.repetition && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<RepeatIcon />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<RepeatIcon />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
text={makeRecurrenceString(event)}
|
text={makeRecurrenceString(event)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@@ -560,7 +627,11 @@ export default function EventPreviewModal({
|
|||||||
{/* Error */}
|
{/* Error */}
|
||||||
{event.error && (
|
{event.error && (
|
||||||
<InfoRow
|
<InfoRow
|
||||||
icon={<ErrorOutlineIcon color="error" style={{ fontSize: 18 }} />}
|
icon={
|
||||||
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<ErrorOutlineIcon color="error" />
|
||||||
|
</Box>
|
||||||
|
}
|
||||||
text={event.error}
|
text={event.error}
|
||||||
error
|
error
|
||||||
/>
|
/>
|
||||||
@@ -574,7 +645,9 @@ export default function EventPreviewModal({
|
|||||||
marginBottom: 2,
|
marginBottom: 2,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||||
|
<CalendarTodayIcon />
|
||||||
|
</Box>
|
||||||
<CircleIcon
|
<CircleIcon
|
||||||
style={{
|
style={{
|
||||||
color: calendar.color?.light ?? "#3788D8",
|
color: calendar.color?.light ?? "#3788D8",
|
||||||
@@ -582,13 +655,8 @@ export default function EventPreviewModal({
|
|||||||
height: 12,
|
height: 12,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<Typography variant="body2">{calendar.name}</Typography>
|
<Typography>{calendar.name}</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
{currentUserAttendee && !event.repetition && (
|
|
||||||
<>
|
|
||||||
<Divider variant="fullWidth" />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ResponsiveDialog>
|
</ResponsiveDialog>
|
||||||
<EditModeDialog
|
<EditModeDialog
|
||||||
type={openEditModePopup}
|
type={openEditModePopup}
|
||||||
@@ -620,6 +688,26 @@ export default function EventPreviewModal({
|
|||||||
calId={calId}
|
calId={calId}
|
||||||
typeOfAction={typeOfAction}
|
typeOfAction={typeOfAction}
|
||||||
/>
|
/>
|
||||||
|
<EventPopover
|
||||||
|
anchorEl={null}
|
||||||
|
open={openDuplicateModal}
|
||||||
|
selectedRange={
|
||||||
|
{
|
||||||
|
start: new Date(event.start),
|
||||||
|
startStr: event.start,
|
||||||
|
end: new Date(event.end ?? ""),
|
||||||
|
endStr: event.end ?? "",
|
||||||
|
allDay: event.allday ?? false,
|
||||||
|
} as DateSelectArg
|
||||||
|
}
|
||||||
|
setSelectedRange={() => {}}
|
||||||
|
calendarRef={{ current: null }}
|
||||||
|
onClose={() => {
|
||||||
|
setOpenDuplicateModal(false);
|
||||||
|
onClose({}, "backdropClick"); // Đóng cả preview modal
|
||||||
|
}}
|
||||||
|
event={event}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@ function EventPopover({
|
|||||||
);
|
);
|
||||||
const [attendees, setAttendees] = useState<userAttendee[]>(
|
const [attendees, setAttendees] = useState<userAttendee[]>(
|
||||||
event?.attendee
|
event?.attendee
|
||||||
? event.attendee.filter((a) => a.cal_address !== organizer.cal_address)
|
? event.attendee.filter((a) => a.cal_address !== organizer?.cal_address)
|
||||||
: []
|
: []
|
||||||
);
|
);
|
||||||
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
const [alarm, setAlarm] = useState(event?.alarm?.trigger ?? "");
|
||||||
@@ -482,8 +482,8 @@ function EventPopover({
|
|||||||
timezone,
|
timezone,
|
||||||
attendee: [
|
attendee: [
|
||||||
{
|
{
|
||||||
cn: organizer.cn,
|
cn: organizer?.cn ?? "",
|
||||||
cal_address: organizer.cal_address,
|
cal_address: organizer?.cal_address ?? "",
|
||||||
partstat: "ACCEPTED",
|
partstat: "ACCEPTED",
|
||||||
rsvp: "FALSE",
|
rsvp: "FALSE",
|
||||||
role: "CHAIR",
|
role: "CHAIR",
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
# Custom MUI Theme
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This project uses a custom MUI theme to customize Typography, colors and components according to Twake Calendar design.
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/theme/
|
||||||
|
├── theme.ts # Custom theme definition
|
||||||
|
├── ThemeProvider.tsx # ThemeProvider wrapper
|
||||||
|
└── README.md # This documentation
|
||||||
|
```
|
||||||
|
|
||||||
|
## Custom Theme Features
|
||||||
|
|
||||||
|
### Colors
|
||||||
|
|
||||||
|
- **Text Primary**: `#000000` (Black)
|
||||||
|
- **Text Secondary**: `#717D96` (Custom gray)
|
||||||
|
- **Text Secondary Container**: `#243B55` (Dark gray)
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
- **Caption**: fontSize 13px, color `#717D96`
|
||||||
|
|
||||||
|
### Components
|
||||||
|
|
||||||
|
- **Typography**: Custom styled caption variant
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
### Using CustomThemeProvider
|
||||||
|
|
||||||
|
Import and use `CustomThemeProvider` to wrap your application:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { CustomThemeProvider } from "./theme/ThemeProvider";
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
return <CustomThemeProvider>{/* Your application */}</CustomThemeProvider>;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Accessing the Theme
|
||||||
|
|
||||||
|
Use `useTheme` to access theme variables:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
function MyComponent() {
|
||||||
|
const theme = useTheme();
|
||||||
|
const textColor = theme.palette.text.secondary;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### CSS Variables
|
||||||
|
|
||||||
|
Use theme variables directly in CSS:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Typography sx={{ color: "text.secondary" }}>
|
||||||
|
Text with secondary color
|
||||||
|
</Typography>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Typography
|
||||||
|
|
||||||
|
Use predefined typography variants:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<Typography variant="caption">Caption text</Typography>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customizing Styles
|
||||||
|
|
||||||
|
Modify styles in `theme.ts` to customize components:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
components: {
|
||||||
|
MuiTypography: {
|
||||||
|
styleOverrides: {
|
||||||
|
caption: {
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#717D96",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Migration to Cozy UI
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install cozy-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Replacing ThemeProvider
|
||||||
|
|
||||||
|
In your main file, replace `CustomThemeProvider` with `MuiCozyTheme`:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
import { MuiCozyTheme } from "cozy-ui/React/MuiCozyTheme";
|
||||||
|
|
||||||
|
<MuiCozyTheme>{children}</MuiCozyTheme>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Updating Palette
|
||||||
|
|
||||||
|
Adapt the color palette in `theme.ts` to use Cozy UI colors. Cozy UI provides its own colors that can be integrated into the theme configuration.
|
||||||
|
|
||||||
|
### Component Migration
|
||||||
|
|
||||||
|
Update components to use Cozy UI components instead of standard MUI components, following Cozy UI documentation for specific components.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { ThemeProvider } from "@mui/material/styles";
|
||||||
|
import { CssBaseline } from "@mui/material";
|
||||||
|
import { customTheme } from "./theme";
|
||||||
|
|
||||||
|
interface CustomThemeProviderProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function CustomThemeProvider({ children }: CustomThemeProviderProps) {
|
||||||
|
return (
|
||||||
|
<ThemeProvider theme={customTheme}>
|
||||||
|
<CssBaseline />
|
||||||
|
{children}
|
||||||
|
</ThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { customTheme };
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { createTheme } from "@mui/material/styles";
|
||||||
|
|
||||||
|
// Custom theme for Twake Calendar
|
||||||
|
export const customTheme = createTheme({
|
||||||
|
palette: {
|
||||||
|
text: {
|
||||||
|
primary: "#000000",
|
||||||
|
secondary: "#717D96",
|
||||||
|
secondaryContainer: "#243B55",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
typography: {
|
||||||
|
caption: {
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
fontWeight: 400,
|
||||||
|
lineHeight: 1.4,
|
||||||
|
color: "#8C9CAF", // Custom secondary text color
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
// Custom Typography component
|
||||||
|
MuiTypography: {
|
||||||
|
styleOverrides: {
|
||||||
|
// Custom variant for event info text
|
||||||
|
caption: {
|
||||||
|
fontSize: "13px",
|
||||||
|
color: "#717D96",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// Custom Button component
|
||||||
|
// Custom Dialog component
|
||||||
|
// Custom DialogActions component
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Export theme type for TypeScript
|
||||||
|
export type CustomTheme = typeof customTheme;
|
||||||
Reference in New Issue
Block a user