diff --git a/__test__/features/Calendars/CalendarAPI.test.tsx b/__test__/features/Calendars/CalendarAPI.test.tsx index 2c584a2..1acec9a 100644 --- a/__test__/features/Calendars/CalendarAPI.test.tsx +++ b/__test__/features/Calendars/CalendarAPI.test.tsx @@ -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", }), }); }); diff --git a/package-lock.json b/package-lock.json index 98c863a..bcd0400 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index df091cb..09bbd5b 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Attendees/PeopleSearch.tsx b/src/components/Attendees/PeopleSearch.tsx index ea5481d..4b244e2 100644 --- a/src/components/Attendees/PeopleSearch.tsx +++ b/src/components/Attendees/PeopleSearch.tsx @@ -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; } 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 ( ; @@ -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) => diff --git a/src/components/Calendar/CalendarColorPicker.tsx b/src/components/Calendar/CalendarColorPicker.tsx index 6d901f2..e80fcc0 100644 --- a/src/components/Calendar/CalendarColorPicker.tsx +++ b/src/components/Calendar/CalendarColorPicker.tsx @@ -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; + colors?: Record[]; + onChange: (color: Record) => void; }) { + const [customColor, setCustomColor] = useState( + !colors.find((c) => c.light === selectedColor?.light) + ? selectedColor + : undefined + ); return ( {colors.map((c) => ( - 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", - }} + ))} + {customColor && ( + + )} + + { + onChange(c); + setCustomColor(c); + }} + selectedColor={selectedColor} + /> ); } +function ColorBox({ + color, + onChange, + selectedColor, +}: { + color: Record; + onChange: (color: Record) => void; + selectedColor: Record; +}) { + return ( + 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", + }} + > + + + + ); +} + +function ColorPickerBox({ + onChange, + selectedColor, +}: { + onChange: (color: Record) => void; + selectedColor: Record; +}) { + const [oldColor] = useState( + selectedColor ?? { light: "#ffffff", dark: "#808080" } + ); + const [color, setColor] = useState(oldColor); + + const [anchorEl, setAnchorEl] = useState(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 ( + <> + + + + + + + Choose custom colour + + + Choose a background colour for this calendar + + + + + + + + + Hex + + handleColorChange(e.target.value)} + variant="standard" + size="small" + slotProps={{ inputLabel: { shrink: true } }} + /> + + + + + + + + + ); +} diff --git a/src/components/Calendar/CalendarModal.tsx b/src/components/Calendar/CalendarModal.tsx index fc4e528..97fffbb 100644 --- a/src/components/Calendar/CalendarModal.tsx +++ b/src/components/Calendar/CalendarModal.tsx @@ -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>(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, 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"); }; diff --git a/src/components/Calendar/CalendarSearch.tsx b/src/components/Calendar/CalendarSearch.tsx index b1d4d1e..97db2e8 100644 --- a/src/components/Calendar/CalendarSearch.tsx +++ b/src/components/Calendar/CalendarSearch.tsx @@ -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; @@ -29,8 +32,10 @@ function CalendarItem({ }: { cal: CalendarWithOwner; onRemove: () => void; - onColorChange: (color: string) => void; + onColorChange: (color: Record) => void; }) { + const theme = useTheme(); + return ( @@ -85,7 +93,10 @@ function SelectedCalendarsList({ calendars: Record; selectedCal: CalendarWithOwner[]; onRemove: (cal: CalendarWithOwner) => void; - onColorChange: (cal: CalendarWithOwner, color: string) => void; + onColorChange: ( + cal: CalendarWithOwner, + color: Record + ) => 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 ( - { onClose({}, "backdropClick"); setSelectedCalendars([]); setSelectedUsers([]); }} + title="Browse other calendars" + actions={ + <> + + + + } > - { + setSelectedUsers(value); + + const cals = await Promise.all( + value.map(async (user: User) => { + const cals = (await getCalendars( + user.openpaasId, + "sharedPublic=true&withRights=true" + )) as Record; + return cals._embedded?.["dav:calendar"] + ? cals._embedded["dav:calendar"].map( + (cal: Record) => ({ cal, owner: user }) + ) + : { cal: undefined, owner: user }; + }) + ); + + setSelectedCalendars(cals.flat()); }} - > - - - onClose({}, "backdropClick")} - > - - - + /> - - - { - setSelectedUsers(value); - - const cals = await Promise.all( - value.map(async (user: User) => { - const cals = (await getCalendars( - user.openpaasId, - "sharedPublic=true&withRights=true" - )) as Record; - return cals._embedded?.["dav:calendar"] - ? cals._embedded["dav:calendar"].map( - (cal: Record) => ({ cal, owner: user }) - ) - : { cal: undefined, owner: user }; - }) - ); - - setSelectedCalendars(cals.flat()); - }} - /> - - { - 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 - ) - ) - } - /> - - - - - - - - - + { + 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 + ) + ) + } + /> + ); } diff --git a/src/components/Calendar/CalendarSelection.tsx b/src/components/Calendar/CalendarSelection.tsx index 7a43327..315084f 100644 --- a/src/components/Calendar/CalendarSelection.tsx +++ b/src/components/Calendar/CalendarSelection.tsx @@ -218,8 +218,8 @@ function CalendarSelector({