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", () => {
|
||||
it("opens EventPopover when button clicked", () => {
|
||||
it("calls onOpenDuplicate when button clicked", () => {
|
||||
const handleClose = jest.fn();
|
||||
const onOpenDuplicate = jest.fn();
|
||||
renderWithProviders(
|
||||
<EventDuplication
|
||||
event={
|
||||
@@ -71,36 +72,14 @@ describe("EventDuplication", () => {
|
||||
.event1
|
||||
}
|
||||
onClose={handleClose}
|
||||
onOpenDuplicate={onOpenDuplicate}
|
||||
/>,
|
||||
preloadedState
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("menuitem", { name: /Duplicate event/i }));
|
||||
|
||||
expect(screen.getAllByText(/Duplicate Event/i)[1]).toBeInTheDocument();
|
||||
});
|
||||
|
||||
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();
|
||||
expect(onOpenDuplicate).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+14
-10
@@ -8,18 +8,22 @@ import { Loading } from "./components/Loading/Loading";
|
||||
import HandleLogin from "./features/User/HandleLogin";
|
||||
import CalendarLayout from "./components/Calendar/CalendarLayout";
|
||||
import { Error } from "./components/Error/Error";
|
||||
import { CustomThemeProvider } from "./theme/ThemeProvider";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HandleLogin />} />
|
||||
<Route path="/calendar" element={<CalendarLayout />} />
|
||||
<Route path="/callback" element={<CallbackResume />} />
|
||||
<Route path="/error" element={<Error />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Suspense>
|
||||
<CustomThemeProvider>
|
||||
<Suspense fallback={<Loading />}>
|
||||
<Router history={history}>
|
||||
<Routes>
|
||||
<Route path="/" element={<HandleLogin />} />
|
||||
<Route path="/calendar" element={<CalendarLayout />} />
|
||||
<Route path="/callback" element={<CallbackResume />} />
|
||||
<Route path="/error" element={<Error />} />
|
||||
</Routes>
|
||||
</Router>
|
||||
</Suspense>
|
||||
</CustomThemeProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,17 +66,19 @@ export function TimezoneSelector({ value, onChange }: TimezoneSelectProps) {
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
sx: { width: 280, maxHeight: 400, overflow: "auto", p: 1 },
|
||||
sx: { width: 280, maxHeight: 400, overflow: "hidden", p: 0 },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<TimezoneAutocomplete
|
||||
size="medium"
|
||||
value={effectiveTimezone}
|
||||
onChange={onChange}
|
||||
zones={timezoneList.zones}
|
||||
getTimezoneOffset={getTimezoneOffset}
|
||||
autoFocus={true}
|
||||
inputFontSize="10px"
|
||||
showIcon={true}
|
||||
inputFontSize="14px"
|
||||
inputPadding="2px 4px"
|
||||
onClose={handleClose}
|
||||
disableClearable={true}
|
||||
|
||||
@@ -78,6 +78,14 @@ interface ResponsiveDialogProps
|
||||
dividers?: boolean;
|
||||
/** Whether to show header action icons (expand/close) in normal mode (default: true) */
|
||||
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({
|
||||
@@ -99,6 +107,8 @@ function ResponsiveDialog({
|
||||
dialogTitleProps,
|
||||
dividers = false,
|
||||
showHeaderActions = true,
|
||||
actionsBorderTop = false,
|
||||
actionsJustifyContent = "flex-end",
|
||||
sx,
|
||||
...otherDialogProps
|
||||
}: ResponsiveDialogProps) {
|
||||
@@ -199,7 +209,20 @@ function ResponsiveDialog({
|
||||
<Stack spacing={currentSpacing}>{children}</Stack>
|
||||
)}
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,51 +1,22 @@
|
||||
import { IconButton, MenuItem } from "@mui/material";
|
||||
import { MenuItem } from "@mui/material";
|
||||
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({
|
||||
onClose,
|
||||
event,
|
||||
onOpenDuplicate,
|
||||
}: {
|
||||
onClose: Function;
|
||||
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 (
|
||||
<>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpenModal(true);
|
||||
}}
|
||||
>
|
||||
Duplicate event
|
||||
</MenuItem>
|
||||
<EventPopover
|
||||
anchorEl={null}
|
||||
open={openModal}
|
||||
selectedRange={selectedRange}
|
||||
setSelectedRange={setSelectedRange}
|
||||
calendarRef={calendarRef}
|
||||
onClose={handleClosePopover}
|
||||
event={event}
|
||||
/>
|
||||
</>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
onOpenDuplicate?.();
|
||||
}}
|
||||
>
|
||||
Duplicate event
|
||||
</MenuItem>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -77,12 +77,14 @@ export function InfoRow({
|
||||
<Typography
|
||||
variant="body2"
|
||||
color={error ? "error" : "textPrimary"}
|
||||
style={{
|
||||
sx={{
|
||||
wordBreak: "break-word",
|
||||
whiteSpace: "pre-line",
|
||||
maxHeight: "33vh",
|
||||
overflowY: "auto",
|
||||
width: "100%",
|
||||
fontSize: "16px",
|
||||
fontFamily: "'Inter', sans-serif",
|
||||
}}
|
||||
>
|
||||
{data ? (
|
||||
|
||||
@@ -44,6 +44,7 @@ export function renderAttendeeBadge(
|
||||
>
|
||||
<Badge
|
||||
overlap="circular"
|
||||
sx={{ marginRight: 2 }}
|
||||
anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
|
||||
badgeContent={
|
||||
classIcon && (
|
||||
@@ -65,7 +66,6 @@ export function renderAttendeeBadge(
|
||||
</Badge>
|
||||
<Box style={{ display: "flex", flexDirection: "column", minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="body2"
|
||||
noWrap
|
||||
style={{
|
||||
maxWidth: "180px",
|
||||
@@ -76,11 +76,7 @@ export function renderAttendeeBadge(
|
||||
{a.cn || a.cal_address}
|
||||
</Typography>
|
||||
{isOrganizer && (
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
style={{ fontStyle: "italic" }}
|
||||
>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Organizer
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
@@ -12,13 +12,13 @@ import SubjectIcon from "@mui/icons-material/Subject";
|
||||
import VideocamIcon from "@mui/icons-material/Videocam";
|
||||
import RepeatIcon from "@mui/icons-material/Repeat";
|
||||
import LockOutlineIcon from "@mui/icons-material/LockOutline";
|
||||
import EventPopover from "./EventModal";
|
||||
import { DateSelectArg } from "@fullcalendar/core";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
ButtonGroup,
|
||||
Chip,
|
||||
DialogActions,
|
||||
Divider,
|
||||
IconButton,
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -77,6 +77,7 @@ export default function EventPreviewModal({
|
||||
const [showAllAttendees, setShowAllAttendees] = useState(false);
|
||||
const [openFullDisplay, setOpenFullDisplay] = useState(false);
|
||||
const [openUpdateModal, setOpenUpdateModal] = useState(false);
|
||||
const [openDuplicateModal, setOpenDuplicateModal] = useState(false);
|
||||
const [hidePreview, setHidePreview] = useState(false);
|
||||
const [openEditModePopup, setOpenEditModePopup] = useState<string | null>(
|
||||
null
|
||||
@@ -136,6 +137,8 @@ export default function EventPreviewModal({
|
||||
open={open && !hidePreview}
|
||||
onClose={() => onClose({}, "backdropClick")}
|
||||
showHeaderActions={false}
|
||||
actionsBorderTop={true}
|
||||
actionsJustifyContent="center"
|
||||
style={{ overflow: "auto" }}
|
||||
title={
|
||||
event.title && (
|
||||
@@ -210,7 +213,15 @@ export default function EventPreviewModal({
|
||||
Email attendees
|
||||
</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 && (
|
||||
<MenuItem
|
||||
onClick={async () => {
|
||||
@@ -270,10 +281,12 @@ export default function EventPreviewModal({
|
||||
<LockOutlineIcon />
|
||||
))}
|
||||
<Typography
|
||||
variant="inherit"
|
||||
fontWeight="bold"
|
||||
style={{
|
||||
variant="h5"
|
||||
sx={{
|
||||
fontSize: "24px",
|
||||
fontWeight: 600,
|
||||
wordBreak: "break-word",
|
||||
fontFamily: "Inter, sans-serif",
|
||||
}}
|
||||
gutterBottom
|
||||
>
|
||||
@@ -293,7 +306,7 @@ export default function EventPreviewModal({
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
<Typography variant="body2" color="textSecondary" gutterBottom>
|
||||
<Typography color="text.secondaryContainer" gutterBottom>
|
||||
{formatDate(event.start, event.allday)}
|
||||
{event.end &&
|
||||
formatEnd(event.start, event.end, event.allday) &&
|
||||
@@ -305,15 +318,22 @@ export default function EventPreviewModal({
|
||||
actions={
|
||||
<>
|
||||
{currentUserAttendee && (
|
||||
<Box style={{ display: "flex", flexDirection: "row" }}>
|
||||
<Typography variant="body2">Attending?</Typography>
|
||||
<ButtonGroup size="small" fullWidth>
|
||||
<>
|
||||
<Typography sx={{ marginRight: 2 }}>Attending?</Typography>
|
||||
<Box display="flex" gap="15px" alignItems="center">
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "ACCEPTED"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "ACCEPTED"
|
||||
? "success"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
@@ -346,11 +366,18 @@ export default function EventPreviewModal({
|
||||
Accept
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "TENTATIVE"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "TENTATIVE"
|
||||
? "warning"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
@@ -382,11 +409,18 @@ export default function EventPreviewModal({
|
||||
Maybe
|
||||
</Button>
|
||||
<Button
|
||||
variant={
|
||||
currentUserAttendee?.partstat === "DECLINED"
|
||||
? "contained"
|
||||
: "outlined"
|
||||
}
|
||||
color={
|
||||
currentUserAttendee?.partstat === "DECLINED"
|
||||
? "error"
|
||||
: "primary"
|
||||
}
|
||||
size="large"
|
||||
sx={{ borderRadius: "50px" }}
|
||||
onClick={() => {
|
||||
if (isRecurring) {
|
||||
setAfterChoiceFunc(
|
||||
@@ -417,8 +451,8 @@ export default function EventPreviewModal({
|
||||
>
|
||||
Decline
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Box>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
@@ -428,7 +462,11 @@ export default function EventPreviewModal({
|
||||
{/* Video */}
|
||||
{event.x_openpass_videoconference && (
|
||||
<InfoRow
|
||||
icon={<VideocamIcon style={{ fontSize: 18 }} />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<VideocamIcon />
|
||||
</Box>
|
||||
}
|
||||
content={
|
||||
<Button
|
||||
variant="contained"
|
||||
@@ -445,7 +483,11 @@ export default function EventPreviewModal({
|
||||
{attendees?.length > 0 && (
|
||||
<>
|
||||
<InfoRow
|
||||
icon={<PeopleAltOutlinedIcon />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<PeopleAltOutlinedIcon />
|
||||
</Box>
|
||||
}
|
||||
content={
|
||||
<Box
|
||||
style={{
|
||||
@@ -454,9 +496,11 @@ export default function EventPreviewModal({
|
||||
flexDirection: "row",
|
||||
}}
|
||||
>
|
||||
<Box>
|
||||
<Box sx={{ marginRight: 2 }}>
|
||||
<Typography>{attendees.length} guests</Typography>
|
||||
<Typography>
|
||||
<Typography
|
||||
sx={{ fontSize: "13px", color: "text.secondary" }}
|
||||
>
|
||||
{
|
||||
attendees.filter((a) => a.partstat === "ACCEPTED")
|
||||
.length
|
||||
@@ -488,9 +532,13 @@ export default function EventPreviewModal({
|
||||
</AvatarGroup>
|
||||
)}
|
||||
<Typography
|
||||
variant="body2"
|
||||
color="primary"
|
||||
style={{ cursor: "pointer", marginTop: 0.5 }}
|
||||
sx={{
|
||||
cursor: "pointer",
|
||||
marginLeft: 2,
|
||||
fontSize: "14px",
|
||||
color: "text.secondary",
|
||||
alignSelf: "center",
|
||||
}}
|
||||
onClick={() => setShowAllAttendees(!showAllAttendees)}
|
||||
>
|
||||
{showAllAttendees ? "Show less" : `Show more `}
|
||||
@@ -510,25 +558,44 @@ export default function EventPreviewModal({
|
||||
{/* Location */}
|
||||
{event.location && (
|
||||
<InfoRow
|
||||
icon={<LocationOnOutlinedIcon />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<LocationOnOutlinedIcon />
|
||||
</Box>
|
||||
}
|
||||
text={event.location}
|
||||
/>
|
||||
)}
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<InfoRow icon={<SubjectIcon />} text={event.description} />
|
||||
<InfoRow
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<SubjectIcon />
|
||||
</Box>
|
||||
}
|
||||
text={event.description}
|
||||
/>
|
||||
)}
|
||||
{/* ALARM */}
|
||||
{event.alarm && (
|
||||
<InfoRow
|
||||
icon={<NotificationsNoneIcon />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<NotificationsNoneIcon />
|
||||
</Box>
|
||||
}
|
||||
text={`${event.alarm.trigger} before by ${event.alarm.action}`}
|
||||
/>
|
||||
)}
|
||||
{/* Repetition */}
|
||||
{event.repetition && (
|
||||
<InfoRow
|
||||
icon={<RepeatIcon />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<RepeatIcon />
|
||||
</Box>
|
||||
}
|
||||
text={makeRecurrenceString(event)}
|
||||
/>
|
||||
)}
|
||||
@@ -560,7 +627,11 @@ export default function EventPreviewModal({
|
||||
{/* Error */}
|
||||
{event.error && (
|
||||
<InfoRow
|
||||
icon={<ErrorOutlineIcon color="error" style={{ fontSize: 18 }} />}
|
||||
icon={
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<ErrorOutlineIcon color="error" />
|
||||
</Box>
|
||||
}
|
||||
text={event.error}
|
||||
error
|
||||
/>
|
||||
@@ -574,7 +645,9 @@ export default function EventPreviewModal({
|
||||
marginBottom: 2,
|
||||
}}
|
||||
>
|
||||
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
||||
<Box sx={{ minWidth: "25px", marginRight: 2 }}>
|
||||
<CalendarTodayIcon />
|
||||
</Box>
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: calendar.color?.light ?? "#3788D8",
|
||||
@@ -582,13 +655,8 @@ export default function EventPreviewModal({
|
||||
height: 12,
|
||||
}}
|
||||
/>
|
||||
<Typography variant="body2">{calendar.name}</Typography>
|
||||
<Typography>{calendar.name}</Typography>
|
||||
</Box>
|
||||
{currentUserAttendee && !event.repetition && (
|
||||
<>
|
||||
<Divider variant="fullWidth" />
|
||||
</>
|
||||
)}
|
||||
</ResponsiveDialog>
|
||||
<EditModeDialog
|
||||
type={openEditModePopup}
|
||||
@@ -620,6 +688,26 @@ export default function EventPreviewModal({
|
||||
calId={calId}
|
||||
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[]>(
|
||||
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 ?? "");
|
||||
@@ -482,8 +482,8 @@ function EventPopover({
|
||||
timezone,
|
||||
attendee: [
|
||||
{
|
||||
cn: organizer.cn,
|
||||
cal_address: organizer.cal_address,
|
||||
cn: organizer?.cn ?? "",
|
||||
cal_address: organizer?.cal_address ?? "",
|
||||
partstat: "ACCEPTED",
|
||||
rsvp: "FALSE",
|
||||
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