Color picker rework (#232)
Co-authored-by: Camille Moussu <cmoussu@linagora.com>
This commit is contained in:
@@ -82,7 +82,7 @@ describe("Calendar API", () => {
|
||||
it("patch Calendar", async () => {
|
||||
const calId = "calId";
|
||||
const calLink = "/calendars/calId.json";
|
||||
const color = "calId";
|
||||
const color = { light: "calIdLight", dark: "calIdDark" };
|
||||
const name = "new cal";
|
||||
const desc = "desc";
|
||||
|
||||
@@ -96,7 +96,7 @@ describe("Calendar API", () => {
|
||||
body: JSON.stringify({
|
||||
"dav:name": "new cal",
|
||||
"caldav:description": "desc",
|
||||
"apple:color": "calId",
|
||||
"apple:color": "calIdLight",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
Generated
+11
@@ -31,6 +31,7 @@
|
||||
"moment-timezone": "^0.5.48",
|
||||
"openid-client": "^6.5.3",
|
||||
"react": "^19.1.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
@@ -11795,6 +11796,16 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-colorful": {
|
||||
"version": "5.6.1",
|
||||
"resolved": "https://registry.npmjs.org/react-colorful/-/react-colorful-5.6.1.tgz",
|
||||
"integrity": "sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "19.1.0",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
"moment-timezone": "^0.5.48",
|
||||
"openid-client": "^6.5.3",
|
||||
"react": "^19.1.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-i18next": "^13.5.0",
|
||||
"react-redux": "^9.2.0",
|
||||
|
||||
@@ -10,13 +10,14 @@ import { searchUsers } from "../../features/User/userAPI";
|
||||
import PeopleOutlineOutlinedIcon from "@mui/icons-material/PeopleOutlineOutlined";
|
||||
import Chip from "@mui/material/Chip";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { getAccessiblePair } from "../Calendar/utils/calendarColorsUtils";
|
||||
|
||||
export interface User {
|
||||
email: string;
|
||||
displayName: string;
|
||||
avatarUrl: string;
|
||||
openpaasId: string;
|
||||
color?: string;
|
||||
color?: Record<string, string>;
|
||||
}
|
||||
|
||||
export function PeopleSearch({
|
||||
@@ -150,8 +151,8 @@ export function PeopleSearch({
|
||||
const label = isString ? option : option.displayName || option.email;
|
||||
const chipColor = isString
|
||||
? theme.palette.grey[300]
|
||||
: (option.color ?? theme.palette.grey[300]);
|
||||
const textColor = theme.palette.getContrastText(chipColor);
|
||||
: (option.color?.light ?? theme.palette.grey[300]);
|
||||
const textColor = getAccessiblePair(chipColor, theme);
|
||||
|
||||
return (
|
||||
<Chip
|
||||
|
||||
@@ -40,6 +40,8 @@ import momentTimezonePlugin from "@fullcalendar/moment-timezone";
|
||||
import { TimezoneSelector } from "./TimezoneSelector";
|
||||
import { MiniCalendar } from "./MiniCalendar";
|
||||
import { User } from "../Attendees/PeopleSearch";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { updateDarkColor } from "./utils/calendarColorsUtils";
|
||||
|
||||
interface CalendarAppProps {
|
||||
calendarRef: React.RefObject<CalendarApi | null>;
|
||||
@@ -58,7 +60,7 @@ export default function CalendarApp({
|
||||
const userId =
|
||||
useAppSelector((state) => state.user.userData?.openpaasId) ?? "";
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const theme = useTheme();
|
||||
useEffect(() => {
|
||||
if (!tokens || !userId) {
|
||||
dispatch(push("/"));
|
||||
@@ -108,6 +110,15 @@ export default function CalendarApp({
|
||||
}
|
||||
}, [calendars, userId]);
|
||||
|
||||
useEffect(() => {
|
||||
updateDarkColor(calendars, theme, dispatch);
|
||||
}, [
|
||||
theme,
|
||||
Object.values(calendars)
|
||||
.map((c) => c.color?.dark)
|
||||
.join(","),
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const validCalendarIds = new Set(Object.keys(calendars));
|
||||
setSelectedCalendars((prev) =>
|
||||
|
||||
@@ -1,36 +1,241 @@
|
||||
import { Box } from "@mui/material";
|
||||
|
||||
const defaultColors = ["#34d399", "#fbbf24", "#f87171", "#60a5fa", "#f472b6"];
|
||||
import AddIcon from "@mui/icons-material/Add";
|
||||
import CheckIcon from "@mui/icons-material/Check";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Popover,
|
||||
TextField,
|
||||
Typography,
|
||||
useTheme,
|
||||
} from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { HexColorPicker } from "react-colorful";
|
||||
import { defaultColors, getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||
|
||||
export function ColorPicker({
|
||||
selectedColor,
|
||||
colors = defaultColors,
|
||||
onChange,
|
||||
}: {
|
||||
selectedColor?: string;
|
||||
colors?: string[];
|
||||
onChange: (color: string) => void;
|
||||
selectedColor: Record<string, string>;
|
||||
colors?: Record<string, string>[];
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
}) {
|
||||
const [customColor, setCustomColor] = useState(
|
||||
!colors.find((c) => c.light === selectedColor?.light)
|
||||
? selectedColor
|
||||
: undefined
|
||||
);
|
||||
return (
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
{colors.map((c) => (
|
||||
<Box
|
||||
key={c}
|
||||
role="button"
|
||||
aria-label={`select color ${c}`}
|
||||
onClick={() => onChange(c)}
|
||||
style={{
|
||||
width: 20,
|
||||
height: 20,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: c,
|
||||
cursor: "pointer",
|
||||
border:
|
||||
selectedColor === c ? "2px solid black" : "2px solid transparent",
|
||||
transition: "all 0.2s",
|
||||
}}
|
||||
<ColorBox
|
||||
key={c.light}
|
||||
color={c}
|
||||
onChange={onChange}
|
||||
selectedColor={selectedColor}
|
||||
/>
|
||||
))}
|
||||
{customColor && (
|
||||
<ColorBox
|
||||
color={customColor ?? {}}
|
||||
onChange={onChange}
|
||||
selectedColor={selectedColor}
|
||||
/>
|
||||
)}
|
||||
|
||||
<ColorPickerBox
|
||||
onChange={(c) => {
|
||||
onChange(c);
|
||||
setCustomColor(c);
|
||||
}}
|
||||
selectedColor={selectedColor}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
function ColorBox({
|
||||
color,
|
||||
onChange,
|
||||
selectedColor,
|
||||
}: {
|
||||
color: Record<string, string>;
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
selectedColor: Record<string, string>;
|
||||
}) {
|
||||
return (
|
||||
<Box
|
||||
role="button"
|
||||
aria-label={`select color ${color.light}`}
|
||||
onClick={() => onChange(color)}
|
||||
style={{
|
||||
width: "46px",
|
||||
height: "32px",
|
||||
padding: 0,
|
||||
borderRadius: "4px",
|
||||
backgroundColor: color.light,
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "7px",
|
||||
width: "100%",
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
backgroundColor: color.dark,
|
||||
}}
|
||||
></Box>
|
||||
<CheckIcon
|
||||
style={{
|
||||
visibility:
|
||||
selectedColor?.light === color.light ? "visible" : "hidden",
|
||||
color: color.dark,
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
function ColorPickerBox({
|
||||
onChange,
|
||||
selectedColor,
|
||||
}: {
|
||||
onChange: (color: Record<string, string>) => void;
|
||||
selectedColor: Record<string, string>;
|
||||
}) {
|
||||
const [oldColor] = useState(
|
||||
selectedColor ?? { light: "#ffffff", dark: "#808080" }
|
||||
);
|
||||
const [color, setColor] = useState(oldColor);
|
||||
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
|
||||
const open = Boolean(anchorEl);
|
||||
const theme = useTheme();
|
||||
|
||||
const handleClick = (event: any) => {
|
||||
setAnchorEl(event.currentTarget);
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
onChange(oldColor);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleSave = () => {
|
||||
onChange(color);
|
||||
setAnchorEl(null);
|
||||
};
|
||||
|
||||
const handleColorChange = (c: string) => {
|
||||
const newLight = c;
|
||||
const newColor = {
|
||||
light: newLight,
|
||||
dark: getAccessiblePair(newLight, theme),
|
||||
};
|
||||
setColor(newColor);
|
||||
onChange(newColor);
|
||||
};
|
||||
return (
|
||||
<>
|
||||
<Box
|
||||
key={"colorPicker"}
|
||||
role="button"
|
||||
aria-label={`select custom color`}
|
||||
onClick={handleClick}
|
||||
style={{
|
||||
width: "46px",
|
||||
height: "32px",
|
||||
padding: 0,
|
||||
borderRadius: "4px",
|
||||
border: "1px solid #CBD2E0",
|
||||
backgroundColor: "#FFF",
|
||||
cursor: "pointer",
|
||||
transition: "all 0.2s",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
height: "7px",
|
||||
width: "100%",
|
||||
borderRadius: "4px 4px 0px 0px",
|
||||
backgroundColor: "#CBD2E0",
|
||||
}}
|
||||
></Box>
|
||||
<AddIcon
|
||||
style={{
|
||||
color: "#CBD2E0",
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
<Popover
|
||||
open={open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={handleClose}
|
||||
anchorOrigin={{
|
||||
vertical: "center",
|
||||
horizontal: "center",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
slotProps={{
|
||||
paper: {
|
||||
style: {
|
||||
padding: "24px",
|
||||
width: "294px",
|
||||
borderRadius: "8px",
|
||||
boxShadow: "0px 1px 3px #3C404326",
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Typography variant="subtitle1" fontWeight="600">
|
||||
Choose custom colour
|
||||
</Typography>
|
||||
<Typography variant="body2" sx={{ mb: 2, color: "text.secondary" }}>
|
||||
Choose a background colour for this calendar
|
||||
</Typography>
|
||||
|
||||
<Box sx={{ mb: 2 }}>
|
||||
<HexColorPicker
|
||||
color={color.light}
|
||||
onChange={handleColorChange}
|
||||
style={{ width: "100%" }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", alignItems: "center", mb: 2 }}>
|
||||
<Typography variant="body2" sx={{ mr: 1 }}>
|
||||
Hex
|
||||
</Typography>
|
||||
<TextField
|
||||
value={color.light?.toUpperCase()}
|
||||
onChange={(e) => handleColorChange(e.target.value)}
|
||||
variant="standard"
|
||||
size="small"
|
||||
slotProps={{ inputLabel: { shrink: true } }}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Box sx={{ display: "flex", justifyContent: "flex-end", gap: 1 }}>
|
||||
<Button onClick={handleClose}>Cancel</Button>
|
||||
<Button
|
||||
variant="contained"
|
||||
onClick={handleSave}
|
||||
sx={{ textTransform: "none" }}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</Box>
|
||||
</Popover>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ResponsiveDialog } from "../Dialog";
|
||||
import { AccessTab } from "./AccessTab";
|
||||
import { ImportTab } from "./ImportTab";
|
||||
import { SettingsTab } from "./SettingsTab";
|
||||
import { defaultColors } from "./utils/calendarColorsUtils";
|
||||
|
||||
function CalendarPopover({
|
||||
open,
|
||||
@@ -34,7 +35,7 @@ function CalendarPopover({
|
||||
// existing calendar params
|
||||
const [name, setName] = useState("");
|
||||
const [description, setDescription] = useState("");
|
||||
const [color, setColor] = useState("");
|
||||
const [color, setColor] = useState<Record<string, string>>(defaultColors[0]);
|
||||
const [visibility, setVisibility] = useState<"private" | "public">("public");
|
||||
|
||||
// import tab state
|
||||
@@ -45,7 +46,7 @@ function CalendarPopover({
|
||||
// new calendar params (for import new)
|
||||
const [newCalName, setNewCalName] = useState("");
|
||||
const [newCalDescription, setNewCalDescription] = useState("");
|
||||
const [newCalColor, setNewCalColor] = useState("");
|
||||
const [newCalColor, setNewCalColor] = useState(defaultColors[0]);
|
||||
const [newCalVisibility, setNewCalVisibility] = useState<
|
||||
"public" | "private"
|
||||
>("public");
|
||||
@@ -55,13 +56,13 @@ function CalendarPopover({
|
||||
if (calendar) {
|
||||
setName(calendar.name);
|
||||
setDescription(calendar.description ?? "");
|
||||
setColor(calendar.color ?? "");
|
||||
setColor(calendar.color ?? defaultColors[0]);
|
||||
setVisibility(calendar.visibility ?? "public");
|
||||
setImportTarget(calendar.id ?? "new");
|
||||
} else {
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor("");
|
||||
setColor(defaultColors[0]);
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
}
|
||||
@@ -90,7 +91,7 @@ function CalendarPopover({
|
||||
calId: string,
|
||||
name: string,
|
||||
desc: string,
|
||||
color: string,
|
||||
color: Record<string, string>,
|
||||
visibility: string
|
||||
) => {
|
||||
await dispatch(
|
||||
@@ -160,7 +161,7 @@ function CalendarPopover({
|
||||
onClose(e, reason);
|
||||
setName("");
|
||||
setDescription("");
|
||||
setColor("");
|
||||
setColor(defaultColors[0]);
|
||||
setTab("settings");
|
||||
setVisibility("public");
|
||||
setImportTarget("new");
|
||||
@@ -168,7 +169,7 @@ function CalendarPopover({
|
||||
|
||||
setNewCalName("");
|
||||
setNewCalDescription("");
|
||||
setNewCalColor("");
|
||||
setNewCalColor(defaultColors[0]);
|
||||
setNewCalVisibility("public");
|
||||
};
|
||||
|
||||
|
||||
@@ -16,6 +16,9 @@ import { addSharedCalendarAsync } from "../../features/Calendars/CalendarSlice";
|
||||
import { ColorPicker } from "./CalendarColorPicker";
|
||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { ResponsiveDialog } from "../Dialog";
|
||||
|
||||
interface CalendarWithOwner {
|
||||
cal: Record<string, any>;
|
||||
@@ -29,8 +32,10 @@ function CalendarItem({
|
||||
}: {
|
||||
cal: CalendarWithOwner;
|
||||
onRemove: () => void;
|
||||
onColorChange: (color: string) => void;
|
||||
onColorChange: (color: Record<string, string>) => void;
|
||||
}) {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={cal.owner.email + cal.cal["dav:name"]}
|
||||
@@ -65,7 +70,10 @@ function CalendarItem({
|
||||
|
||||
<Box display="flex" alignItems="center" gap={1}>
|
||||
<ColorPicker
|
||||
selectedColor={cal.cal["apple:color"]}
|
||||
selectedColor={{
|
||||
light: cal.cal["apple:color"],
|
||||
dark: getAccessiblePair(cal.cal["apple:color"], theme),
|
||||
}}
|
||||
onChange={onColorChange}
|
||||
/>
|
||||
<IconButton size="small" onClick={onRemove}>
|
||||
@@ -85,7 +93,10 @@ function SelectedCalendarsList({
|
||||
calendars: Record<string, Calendars>;
|
||||
selectedCal: CalendarWithOwner[];
|
||||
onRemove: (cal: CalendarWithOwner) => void;
|
||||
onColorChange: (cal: CalendarWithOwner, color: string) => void;
|
||||
onColorChange: (
|
||||
cal: CalendarWithOwner,
|
||||
color: Record<string, string>
|
||||
) => void;
|
||||
}) {
|
||||
if (selectedCal.length === 0) return null;
|
||||
|
||||
@@ -194,7 +205,13 @@ export default function CalendarSearch({
|
||||
addSharedCalendarAsync({
|
||||
userId: openpaasId,
|
||||
calId,
|
||||
cal: { ...cal, color: cal.cal["apple:color"] },
|
||||
cal: {
|
||||
...cal,
|
||||
color: {
|
||||
light: cal.cal["apple:color"],
|
||||
dark: cal.cal["X-TWAKE-Dark-theme-color"],
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
return cal.cal._links.self.href
|
||||
@@ -212,122 +229,92 @@ export default function CalendarSearch({
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<ResponsiveDialog
|
||||
open={open}
|
||||
contentSx={{ paddingTop: "8px !important" }}
|
||||
onClose={() => {
|
||||
onClose({}, "backdropClick");
|
||||
setSelectedCalendars([]);
|
||||
setSelectedUsers([]);
|
||||
}}
|
||||
title="Browse other calendars"
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onClose({}, "backdropClick")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
Add
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<Box
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: "50%",
|
||||
top: "20%",
|
||||
transform: "translate( -50%)",
|
||||
width: "50vw",
|
||||
maxHeight: "80vh",
|
||||
<PeopleSearch
|
||||
objectTypes={["user"]}
|
||||
selectedUsers={selectedUsers}
|
||||
onChange={async (event: any, value: User[]) => {
|
||||
setSelectedUsers(value);
|
||||
|
||||
const cals = await Promise.all(
|
||||
value.map(async (user: User) => {
|
||||
const cals = (await getCalendars(
|
||||
user.openpaasId,
|
||||
"sharedPublic=true&withRights=true"
|
||||
)) as Record<string, any>;
|
||||
return cals._embedded?.["dav:calendar"]
|
||||
? cals._embedded["dav:calendar"].map(
|
||||
(cal: Record<string, any>) => ({ cal, owner: user })
|
||||
)
|
||||
: { cal: undefined, owner: user };
|
||||
})
|
||||
);
|
||||
|
||||
setSelectedCalendars(cals.flat());
|
||||
}}
|
||||
>
|
||||
<Card
|
||||
style={{
|
||||
padding: 16,
|
||||
borderRadius: 12,
|
||||
boxShadow: "0 4px 12px rgba(0,0,0,0.15)",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
maxHeight: "80vh",
|
||||
}}
|
||||
>
|
||||
<Box style={{ position: "absolute", top: 8, right: 8 }}>
|
||||
<IconButton
|
||||
size="small"
|
||||
onClick={() => onClose({}, "backdropClick")}
|
||||
>
|
||||
<CloseIcon fontSize="small" />
|
||||
</IconButton>
|
||||
</Box>
|
||||
/>
|
||||
|
||||
<CardHeader
|
||||
title="Browse other calendars"
|
||||
style={{ paddingBottom: 0 }}
|
||||
/>
|
||||
<CardContent style={{ flex: 1, overflow: "auto" }}>
|
||||
<PeopleSearch
|
||||
objectTypes={["user"]}
|
||||
selectedUsers={selectedUsers}
|
||||
onChange={async (event: any, value: User[]) => {
|
||||
setSelectedUsers(value);
|
||||
|
||||
const cals = await Promise.all(
|
||||
value.map(async (user: User) => {
|
||||
const cals = (await getCalendars(
|
||||
user.openpaasId,
|
||||
"sharedPublic=true&withRights=true"
|
||||
)) as Record<string, any>;
|
||||
return cals._embedded?.["dav:calendar"]
|
||||
? cals._embedded["dav:calendar"].map(
|
||||
(cal: Record<string, any>) => ({ cal, owner: user })
|
||||
)
|
||||
: { cal: undefined, owner: user };
|
||||
})
|
||||
);
|
||||
|
||||
setSelectedCalendars(cals.flat());
|
||||
}}
|
||||
/>
|
||||
|
||||
<SelectedCalendarsList
|
||||
calendars={calendars}
|
||||
selectedCal={selectedCal}
|
||||
onRemove={(cal) => {
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.filter(
|
||||
(c) => c.cal._links.self.href !== cal.cal._links.self.href
|
||||
)
|
||||
);
|
||||
if (
|
||||
!selectedCal.find(
|
||||
(c) =>
|
||||
cal.owner.email === c.owner.email &&
|
||||
c.cal._links.self.href !== cal.cal._links.self.href
|
||||
)
|
||||
) {
|
||||
setSelectedUsers((prev) =>
|
||||
prev.filter((u) => u.email !== cal.owner.email)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onColorChange={(cal, color) =>
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.map((prevcal) =>
|
||||
prevcal.owner.email === cal.owner.email &&
|
||||
prevcal.cal._links.self.href === cal.cal._links.self.href
|
||||
? {
|
||||
...prevcal,
|
||||
cal: { ...prevcal.cal, "apple:color": color },
|
||||
}
|
||||
: prevcal
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</CardContent>
|
||||
|
||||
<CardActions style={{ justifyContent: "flex-end", gap: 8 }}>
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={() => onClose({}, "backdropClick")}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="contained" onClick={handleSave}>
|
||||
Add
|
||||
</Button>
|
||||
</CardActions>
|
||||
</Card>
|
||||
</Box>
|
||||
</Modal>
|
||||
<SelectedCalendarsList
|
||||
calendars={calendars}
|
||||
selectedCal={selectedCal}
|
||||
onRemove={(cal) => {
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.filter(
|
||||
(c) => c.cal._links.self.href !== cal.cal._links.self.href
|
||||
)
|
||||
);
|
||||
if (
|
||||
!selectedCal.find(
|
||||
(c) =>
|
||||
cal.owner.email === c.owner.email &&
|
||||
c.cal._links.self.href !== cal.cal._links.self.href
|
||||
)
|
||||
) {
|
||||
setSelectedUsers((prev) =>
|
||||
prev.filter((u) => u.email !== cal.owner.email)
|
||||
);
|
||||
}
|
||||
}}
|
||||
onColorChange={(cal, color) =>
|
||||
setSelectedCalendars((prev) =>
|
||||
prev.map((prevcal) =>
|
||||
prevcal.owner.email === cal.owner.email &&
|
||||
prevcal.cal._links.self.href === cal.cal._links.self.href
|
||||
? {
|
||||
...prevcal,
|
||||
cal: {
|
||||
...prevcal.cal,
|
||||
"apple:color": color.light,
|
||||
"X-TWAKE-Dark-theme-color": color.dark,
|
||||
},
|
||||
}
|
||||
: prevcal
|
||||
)
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ResponsiveDialog>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -218,8 +218,8 @@ function CalendarSelector({
|
||||
<label>
|
||||
<Checkbox
|
||||
sx={{
|
||||
color: calendars[id].color,
|
||||
"&.Mui-checked": { color: calendars[id].color },
|
||||
color: calendars[id].color?.light,
|
||||
"&.Mui-checked": { color: calendars[id].color?.light },
|
||||
}}
|
||||
size="small"
|
||||
checked={selectedCalendars.includes(id)}
|
||||
@@ -232,7 +232,14 @@ function CalendarSelector({
|
||||
</IconButton>
|
||||
</div>
|
||||
<Menu id={id} anchorEl={anchorEl} open={open} onClose={handleClose}>
|
||||
<MenuItem onClick={() => setOpen()}>Modify</MenuItem>
|
||||
<MenuItem
|
||||
onClick={() => {
|
||||
setOpen();
|
||||
handleClose();
|
||||
}}
|
||||
>
|
||||
Modify
|
||||
</MenuItem>
|
||||
{!isDefault && <Divider />}
|
||||
{!isDefault && (
|
||||
<MenuItem onClick={() => setDeletePopupOpen(!deletePopupOpen)}>
|
||||
|
||||
@@ -30,7 +30,7 @@ export function ImportTab({
|
||||
setName: Function;
|
||||
description: string;
|
||||
setDescription: Function;
|
||||
color: string;
|
||||
color: Record<string, string>;
|
||||
setColor: Function;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: Function;
|
||||
|
||||
@@ -29,7 +29,7 @@ export function SettingsTab({
|
||||
setName: Function;
|
||||
description: string;
|
||||
setDescription: Function;
|
||||
color: string;
|
||||
color: Record<string, string>;
|
||||
setColor: Function;
|
||||
visibility: "public" | "private";
|
||||
setVisibility: Function;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useRef } from "react";
|
||||
import { useAppDispatch, useAppSelector } from "../../app/hooks";
|
||||
import {
|
||||
@@ -6,6 +7,7 @@ import {
|
||||
} from "../../features/Calendars/CalendarSlice";
|
||||
import { Calendars } from "../../features/Calendars/CalendarTypes";
|
||||
import { User, PeopleSearch } from "../Attendees/PeopleSearch";
|
||||
import { getAccessiblePair } from "./utils/calendarColorsUtils";
|
||||
|
||||
const requestControllers = new Map<string, AbortController>();
|
||||
|
||||
@@ -26,6 +28,7 @@ export function TempCalendarsInput({
|
||||
const tempcalendars =
|
||||
useAppSelector((state) => state.calendars.templist) ?? {};
|
||||
const calendars = useAppSelector((state) => state.calendars.list);
|
||||
const theme = useTheme();
|
||||
|
||||
const prevUsersRef = useRef<User[]>([]);
|
||||
const userColors = new Map<string, string>();
|
||||
@@ -59,8 +62,12 @@ export function TempCalendarsInput({
|
||||
const existingColors = new Set(userColors.values());
|
||||
userColors.set(user.email, generateUserColor(existingColors));
|
||||
}
|
||||
const lightColor = userColors.get(user.email)!;
|
||||
|
||||
user.color = userColors.get(user.email)!;
|
||||
user.color = {
|
||||
light: lightColor,
|
||||
dark: getAccessiblePair(lightColor, theme),
|
||||
};
|
||||
|
||||
dispatch(
|
||||
getTempCalendarsListAsync(user, { signal: controller.signal })
|
||||
|
||||
@@ -127,7 +127,9 @@ export const createViewHandlers = (props: ViewHandlersProps) => {
|
||||
);
|
||||
const delegated = calendar.delegated;
|
||||
let Icon = null;
|
||||
let titleStyle: React.CSSProperties = {};
|
||||
let titleStyle: React.CSSProperties = {
|
||||
color: event._def.extendedProps.colors?.dark,
|
||||
};
|
||||
|
||||
const showSpecialDisplay = attendees.filter((att: userAttendee) =>
|
||||
ownerEmails.has(att.cal_address.toLowerCase())
|
||||
@@ -265,7 +267,7 @@ function RenderEventTitle(
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
backgroundColor: calendar.color,
|
||||
backgroundColor: calendar.color?.light,
|
||||
color: "white",
|
||||
borderRadius: "4px",
|
||||
border: "1px",
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { darken, getContrastRatio, lighten, Theme } from "@mui/material";
|
||||
import { ThunkDispatch } from "@reduxjs/toolkit";
|
||||
import { updateCalColor } from "../../../features/Calendars/CalendarSlice";
|
||||
import { Calendars } from "../../../features/Calendars/CalendarTypes";
|
||||
|
||||
export function updateDarkColor(
|
||||
calendars: Record<string, Calendars>,
|
||||
theme: Theme,
|
||||
dispatch: ThunkDispatch<any, any, any>
|
||||
) {
|
||||
Object.values(calendars).forEach((cal) => {
|
||||
if (!cal?.color?.light) return;
|
||||
const baseColor = cal.color.light;
|
||||
|
||||
const isDefault = Object.values(defaultColors).find(
|
||||
(c) => c.light === baseColor
|
||||
);
|
||||
const darkColor = isDefault
|
||||
? isDefault.dark
|
||||
: getAccessiblePair(baseColor, theme);
|
||||
|
||||
dispatch(
|
||||
updateCalColor({
|
||||
id: cal.id,
|
||||
color: { light: baseColor, dark: darkColor },
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export function getAccessiblePair(baseColor: string, theme: Theme): string {
|
||||
const contrastToBlack = getContrastRatio(baseColor, "#000");
|
||||
const contrastToWhite = getContrastRatio(baseColor, "#fff");
|
||||
const isLight = contrastToBlack > contrastToWhite;
|
||||
|
||||
const adjusted = isLight ? darken(baseColor, 0.6) : lighten(baseColor, 0.7);
|
||||
|
||||
// Check if contrast meets 4.5
|
||||
const contrast = getContrastRatio(baseColor, adjusted);
|
||||
console.log(isLight, contrast);
|
||||
if (contrast >= 4.5) return adjusted;
|
||||
|
||||
if (isLight) {
|
||||
return "#ffffffff";
|
||||
}
|
||||
|
||||
return theme.palette.getContrastText(baseColor);
|
||||
}
|
||||
|
||||
export const defaultColors = [
|
||||
{ light: "#D0ECDA", dark: "#329655" },
|
||||
{ light: "#FAE3CE", dark: "#E15300" },
|
||||
{ light: "#F5CFD0", dark: "#BE0103" },
|
||||
{ light: "#AFCBEF", dark: "#0654B1" },
|
||||
];
|
||||
@@ -62,9 +62,9 @@ export const eventToFullCalendarFormat = (
|
||||
.concat(filteredTempEvents.map((e) => ({ ...e, temp: true })))
|
||||
.map((e) => {
|
||||
if (e.calId.split("/")[0] === userId) {
|
||||
return { ...e, editable: true };
|
||||
return { ...e, colors: e.color, color: e.color?.light, editable: true };
|
||||
}
|
||||
return { ...e, editable: false };
|
||||
return { ...e, colors: e.color, color: e.color?.light, editable: false };
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@ export async function getCalendar(
|
||||
export async function postCalendar(
|
||||
userId: string,
|
||||
calId: string,
|
||||
color: string,
|
||||
color: Record<string, string>,
|
||||
name: string,
|
||||
desc: string
|
||||
) {
|
||||
@@ -67,7 +67,10 @@ export async function addSharedCalendar(
|
||||
"calendarserver:source": {
|
||||
acl: cal.cal.acl,
|
||||
calendarHomeId: cal.cal.id,
|
||||
color: cal.cal["apple:color"],
|
||||
color: {
|
||||
light: cal.cal["apple:color"],
|
||||
dark: cal.cal["X-TWAKE-Dark-theme-color"],
|
||||
},
|
||||
description: cal.cal["caldav:description"],
|
||||
href: cal.cal._links.self.href,
|
||||
id: cal.cal.id,
|
||||
@@ -81,7 +84,7 @@ export async function addSharedCalendar(
|
||||
|
||||
export async function proppatchCalendar(
|
||||
calLink: string,
|
||||
patch: { name: string; desc: string; color: string }
|
||||
patch: { name: string; desc: string; color: Record<string, string> }
|
||||
) {
|
||||
const body: Record<string, string> = {};
|
||||
if (patch.name) {
|
||||
@@ -90,8 +93,8 @@ export async function proppatchCalendar(
|
||||
if (patch.desc) {
|
||||
body["caldav:description"] = patch.desc;
|
||||
}
|
||||
if (patch.color) {
|
||||
body["apple:color"] = patch.color;
|
||||
if (patch.color.light) {
|
||||
body["apple:color"] = patch.color.light;
|
||||
}
|
||||
const response = await api(`dav${calLink}`, {
|
||||
method: "PROPPATCH",
|
||||
|
||||
@@ -53,7 +53,10 @@ export const getCalendarsListAsync = createAsyncThunk<
|
||||
const id = source.replace("/calendars/", "").replace(".json", "");
|
||||
const visibility = getCalendarVisibility(cal["acl"]);
|
||||
const ownerData: any = await getUserDetails(id.split("/")[0]);
|
||||
const color = cal["apple:color"];
|
||||
const color = {
|
||||
light: cal["apple:color"] ?? "#006BD8",
|
||||
dark: cal["X-TWAKE-Dark-theme-color"] ?? "#FFF",
|
||||
};
|
||||
importedCalendars[id] = {
|
||||
id,
|
||||
name,
|
||||
@@ -106,7 +109,10 @@ export const getTempCalendarsListAsync = createAsyncThunk<
|
||||
ownerEmails: ownerData.emails,
|
||||
description,
|
||||
delegated,
|
||||
color: tempUser.color ?? "#a8a8a8ff",
|
||||
color: {
|
||||
light: tempUser.color?.light ?? "#a8a8a8ff",
|
||||
dark: tempUser.color?.dark ?? "#a8a8a8ff",
|
||||
},
|
||||
visibility,
|
||||
events: {},
|
||||
};
|
||||
@@ -157,7 +163,7 @@ export const putEventAsync = createAsyncThunk<
|
||||
return vevents.map((vevent: any[]) => {
|
||||
return parseCalendarEvent(
|
||||
vevent[1],
|
||||
cal.color ?? "",
|
||||
cal.color ?? {},
|
||||
cal.id,
|
||||
eventURL,
|
||||
valarm
|
||||
@@ -187,12 +193,12 @@ export const patchCalendarAsync = createAsyncThunk<
|
||||
{
|
||||
calId: string;
|
||||
calLink: string;
|
||||
patch: { name: string; desc: string; color: string };
|
||||
patch: { name: string; desc: string; color: Record<string, string> };
|
||||
}, // Return type
|
||||
{
|
||||
calId: string;
|
||||
calLink: string;
|
||||
patch: { name: string; desc: string; color: string };
|
||||
patch: { name: string; desc: string; color: Record<string, string> };
|
||||
} // Arg type
|
||||
>("calendars/patchCalendar", async ({ calId, calLink, patch }) => {
|
||||
await proppatchCalendar(calLink, patch);
|
||||
@@ -238,7 +244,7 @@ export const moveEventAsync = createAsyncThunk<
|
||||
const vevents = eventdata.data[2] as any[][];
|
||||
const eventURL = eventdata._links.self.href;
|
||||
return vevents.map((vevent: any[]) => {
|
||||
return parseCalendarEvent(vevent[1], cal.color ?? "", cal.id, eventURL);
|
||||
return parseCalendarEvent(vevent[1], cal.color ?? {}, cal.id, eventURL);
|
||||
});
|
||||
}
|
||||
);
|
||||
@@ -304,13 +310,19 @@ export const createCalendarAsync = createAsyncThunk<
|
||||
{
|
||||
userId: string;
|
||||
calId: string;
|
||||
color: string;
|
||||
color: Record<string, string>;
|
||||
name: string;
|
||||
desc: string;
|
||||
owner: string;
|
||||
ownerEmails: string[];
|
||||
}, // Return type
|
||||
{ userId: string; calId: string; color: string; name: string; desc: string } // Arg type
|
||||
{
|
||||
userId: string;
|
||||
calId: string;
|
||||
color: Record<string, string>;
|
||||
name: string;
|
||||
desc: string;
|
||||
} // Arg type
|
||||
>("calendars/createCalendar", async ({ userId, calId, color, name, desc }) => {
|
||||
await postCalendar(userId, calId, color, name, desc);
|
||||
const ownerData: any = await getUserDetails(userId.split("/")[0]);
|
||||
@@ -331,7 +343,7 @@ export const createCalendarAsync = createAsyncThunk<
|
||||
export const addSharedCalendarAsync = createAsyncThunk<
|
||||
{
|
||||
calId: string;
|
||||
color: string;
|
||||
color: Record<string, string>;
|
||||
link: string;
|
||||
name: string;
|
||||
desc: string;
|
||||
@@ -352,7 +364,10 @@ export const addSharedCalendarAsync = createAsyncThunk<
|
||||
calId: cal.cal._links.self.href
|
||||
.replace("/calendars/", "")
|
||||
.replace(".json", ""),
|
||||
color: cal.cal["apple:color"],
|
||||
color: {
|
||||
light: cal.cal["apple:color"],
|
||||
dark: "#000",
|
||||
},
|
||||
link: `/calendars/${userId}/${calId}.json`,
|
||||
desc: cal.cal["caldav:description"],
|
||||
name: cal.cal["dav:name"],
|
||||
@@ -388,12 +403,15 @@ const CalendarSlice = createSlice({
|
||||
timeZone: string;
|
||||
},
|
||||
reducers: {
|
||||
createCalendar: (state, action: PayloadAction<Record<string, string>>) => {
|
||||
createCalendar: (
|
||||
state,
|
||||
action: PayloadAction<Record<string, string | Record<string, string>>>
|
||||
) => {
|
||||
const id = Date.now().toString(36);
|
||||
state.list[id] = {} as Calendars;
|
||||
state.list[id].name = action.payload.name;
|
||||
state.list[id].color = action.payload.color;
|
||||
state.list[id].description = action.payload.description;
|
||||
state.list[id].name = action.payload.name as string;
|
||||
state.list[id].color = action.payload.color as Record<string, string>;
|
||||
state.list[id].description = action.payload.description as string;
|
||||
state.list[id].events = {};
|
||||
},
|
||||
addEvent: (
|
||||
@@ -450,6 +468,15 @@ const CalendarSlice = createSlice({
|
||||
if (!state.list[action.payload]) return;
|
||||
state.list[action.payload].lastCacheCleared = Date.now();
|
||||
},
|
||||
updateCalColor: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
color: { light: string; dark: string };
|
||||
}>
|
||||
) => {
|
||||
state.list[action.payload.id].color = action.payload.color;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
builder
|
||||
@@ -719,5 +746,6 @@ export const {
|
||||
emptyEventsCal,
|
||||
setTimeZone,
|
||||
clearFetchCache,
|
||||
updateCalColor,
|
||||
} = CalendarSlice.actions;
|
||||
export default CalendarSlice.reducer;
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface Calendars {
|
||||
name: string;
|
||||
delegated?: boolean;
|
||||
prodid?: string;
|
||||
color?: string;
|
||||
color?: Record<string, string>;
|
||||
ownerEmails?: string[];
|
||||
owner: string;
|
||||
description?: string;
|
||||
|
||||
@@ -36,7 +36,7 @@ export async function getEvent(event: CalendarEvent, isMaster?: boolean) {
|
||||
|
||||
const eventjson = parseCalendarEvent(
|
||||
targetVevent[1],
|
||||
event.color ?? "",
|
||||
event.color ?? {},
|
||||
event.calId,
|
||||
event.URL
|
||||
);
|
||||
|
||||
@@ -48,6 +48,7 @@ import { userAttendee } from "../User/userDataTypes";
|
||||
import { getEvent } from "./EventApi";
|
||||
import { formatLocalDateTime } from "../../components/Event/EventFormFields";
|
||||
import { CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import { light } from "@mui/material/styles/createPalette";
|
||||
|
||||
export default function EventDisplayModal({
|
||||
eventId,
|
||||
@@ -224,7 +225,8 @@ export default function EventDisplayModal({
|
||||
<Typography variant="body2">
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: userPersonnalCalendars[index].color ?? "#3788D8",
|
||||
color:
|
||||
userPersonnalCalendars[index].color?.light ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
@@ -238,7 +240,7 @@ export default function EventDisplayModal({
|
||||
<Typography variant="body2">
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: calendars[index].color ?? "#3788D8",
|
||||
color: calendars[index].color?.light ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
|
||||
@@ -577,7 +577,7 @@ export default function EventPreviewModal({
|
||||
<CalendarTodayIcon style={{ fontSize: 16 }} />
|
||||
<CircleIcon
|
||||
style={{
|
||||
color: calendar.color ?? "#3788D8",
|
||||
color: calendar.color?.light ?? "#3788D8",
|
||||
width: 12,
|
||||
height: 12,
|
||||
}}
|
||||
|
||||
@@ -17,7 +17,7 @@ export interface CalendarEvent {
|
||||
attendee: userAttendee[];
|
||||
stamp?: string;
|
||||
sequence?: Number;
|
||||
color?: string;
|
||||
color?: Record<string, string>;
|
||||
allday?: boolean;
|
||||
error?: string;
|
||||
status?: string;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { userAttendee } from "../User/userDataTypes";
|
||||
import { AlarmObject, CalendarEvent } from "./EventsTypes";
|
||||
import { AlarmObject, CalendarEvent, RepetitionObject } from "./EventsTypes";
|
||||
import ICAL from "ical.js";
|
||||
import { TIMEZONES } from "../../utils/timezone-data";
|
||||
import moment from "moment";
|
||||
@@ -7,7 +7,7 @@ type RawEntry = [string, Record<string, string>, string, any];
|
||||
|
||||
export function parseCalendarEvent(
|
||||
data: RawEntry[],
|
||||
color: string,
|
||||
color: Record<string, string>,
|
||||
calendarid: string,
|
||||
eventURL: string,
|
||||
valarm?: RawEntry[]
|
||||
|
||||
Reference in New Issue
Block a user