diff --git a/components/CustomBranding.tsx b/components/CustomBranding.tsx
index 75acc79c79..cb90a1d336 100644
--- a/components/CustomBranding.tsx
+++ b/components/CustomBranding.tsx
@@ -1,5 +1,158 @@
import { useEffect } from "react";
+const brandColor = "#292929";
+const brandTextColor = "#ffffff";
+
+export function colorNameToHex(color: string) {
+ const colors = {
+ aliceblue: "#f0f8ff",
+ antiquewhite: "#faebd7",
+ aqua: "#00ffff",
+ aquamarine: "#7fffd4",
+ azure: "#f0ffff",
+ beige: "#f5f5dc",
+ bisque: "#ffe4c4",
+ black: "#000000",
+ blanchedalmond: "#ffebcd",
+ blue: "#0000ff",
+ blueviolet: "#8a2be2",
+ brown: "#a52a2a",
+ burlywood: "#deb887",
+ cadetblue: "#5f9ea0",
+ chartreuse: "#7fff00",
+ chocolate: "#d2691e",
+ coral: "#ff7f50",
+ cornflowerblue: "#6495ed",
+ cornsilk: "#fff8dc",
+ crimson: "#dc143c",
+ cyan: "#00ffff",
+ darkblue: "#00008b",
+ darkcyan: "#008b8b",
+ darkgoldenrod: "#b8860b",
+ darkgray: "#a9a9a9",
+ darkgreen: "#006400",
+ darkkhaki: "#bdb76b",
+ darkmagenta: "#8b008b",
+ darkolivegreen: "#556b2f",
+ darkorange: "#ff8c00",
+ darkorchid: "#9932cc",
+ darkred: "#8b0000",
+ darksalmon: "#e9967a",
+ darkseagreen: "#8fbc8f",
+ darkslateblue: "#483d8b",
+ darkslategray: "#2f4f4f",
+ darkturquoise: "#00ced1",
+ darkviolet: "#9400d3",
+ deeppink: "#ff1493",
+ deepskyblue: "#00bfff",
+ dimgray: "#696969",
+ dodgerblue: "#1e90ff",
+ firebrick: "#b22222",
+ floralwhite: "#fffaf0",
+ forestgreen: "#228b22",
+ fuchsia: "#ff00ff",
+ gainsboro: "#dcdcdc",
+ ghostwhite: "#f8f8ff",
+ gold: "#ffd700",
+ goldenrod: "#daa520",
+ gray: "#808080",
+ green: "#008000",
+ greenyellow: "#adff2f",
+ honeydew: "#f0fff0",
+ hotpink: "#ff69b4",
+ "indianred ": "#cd5c5c",
+ indigo: "#4b0082",
+ ivory: "#fffff0",
+ khaki: "#f0e68c",
+ lavender: "#e6e6fa",
+ lavenderblush: "#fff0f5",
+ lawngreen: "#7cfc00",
+ lemonchiffon: "#fffacd",
+ lightblue: "#add8e6",
+ lightcoral: "#f08080",
+ lightcyan: "#e0ffff",
+ lightgoldenrodyellow: "#fafad2",
+ lightgrey: "#d3d3d3",
+ lightgreen: "#90ee90",
+ lightpink: "#ffb6c1",
+ lightsalmon: "#ffa07a",
+ lightseagreen: "#20b2aa",
+ lightskyblue: "#87cefa",
+ lightslategray: "#778899",
+ lightsteelblue: "#b0c4de",
+ lightyellow: "#ffffe0",
+ lime: "#00ff00",
+ limegreen: "#32cd32",
+ linen: "#faf0e6",
+ magenta: "#ff00ff",
+ maroon: "#800000",
+ mediumaquamarine: "#66cdaa",
+ mediumblue: "#0000cd",
+ mediumorchid: "#ba55d3",
+ mediumpurple: "#9370d8",
+ mediumseagreen: "#3cb371",
+ mediumslateblue: "#7b68ee",
+ mediumspringgreen: "#00fa9a",
+ mediumturquoise: "#48d1cc",
+ mediumvioletred: "#c71585",
+ midnightblue: "#191970",
+ mintcream: "#f5fffa",
+ mistyrose: "#ffe4e1",
+ moccasin: "#ffe4b5",
+ navajowhite: "#ffdead",
+ navy: "#000080",
+ oldlace: "#fdf5e6",
+ olive: "#808000",
+ olivedrab: "#6b8e23",
+ orange: "#ffa500",
+ orangered: "#ff4500",
+ orchid: "#da70d6",
+ palegoldenrod: "#eee8aa",
+ palegreen: "#98fb98",
+ paleturquoise: "#afeeee",
+ palevioletred: "#d87093",
+ papayawhip: "#ffefd5",
+ peachpuff: "#ffdab9",
+ peru: "#cd853f",
+ pink: "#ffc0cb",
+ plum: "#dda0dd",
+ powderblue: "#b0e0e6",
+ purple: "#800080",
+ rebeccapurple: "#663399",
+ red: "#ff0000",
+ rosybrown: "#bc8f8f",
+ royalblue: "#4169e1",
+ saddlebrown: "#8b4513",
+ salmon: "#fa8072",
+ sandybrown: "#f4a460",
+ seagreen: "#2e8b57",
+ seashell: "#fff5ee",
+ sienna: "#a0522d",
+ silver: "#c0c0c0",
+ skyblue: "#87ceeb",
+ slateblue: "#6a5acd",
+ slategray: "#708090",
+ snow: "#fffafa",
+ springgreen: "#00ff7f",
+ steelblue: "#4682b4",
+ tan: "#d2b48c",
+ teal: "#008080",
+ thistle: "#d8bfd8",
+ tomato: "#ff6347",
+ turquoise: "#40e0d0",
+ violet: "#ee82ee",
+ wheat: "#f5deb3",
+ white: "#ffffff",
+ whitesmoke: "#f5f5f5",
+ yellow: "#ffff00",
+ yellowgreen: "#9acd32",
+ };
+
+ return colors[color.toLowerCase() as keyof typeof colors] !== undefined
+ ? colors[color.toLowerCase() as keyof typeof colors]
+ : false;
+}
+
function computeContrastRatio(a: number[], b: number[]) {
const lum1 = computeLuminance(a[0], a[1], a[2]);
const lum2 = computeLuminance(b[0], b[1], b[2]);
@@ -22,14 +175,30 @@ function hexToRGB(hex: string) {
}
function getContrastingTextColor(bgColor: string | null): string {
- bgColor = bgColor == "" || bgColor == null ? "#292929" : bgColor;
+ bgColor = bgColor == "" || bgColor == null ? brandColor : bgColor;
const rgb = hexToRGB(bgColor);
const whiteContrastRatio = computeContrastRatio(rgb, [255, 255, 255]);
const blackContrastRatio = computeContrastRatio(rgb, [41, 41, 41]); //#292929
- return whiteContrastRatio > blackContrastRatio ? "#ffffff" : "#292929";
+ return whiteContrastRatio > blackContrastRatio ? brandTextColor : brandColor;
}
-const BrandColor = ({ val = "#292929" }: { val: string | undefined | null }) => {
+export function isValidHexCode(val: string | null) {
+ if (val) {
+ val = val.indexOf("#") === 0 ? val : "#" + val;
+ const regex = new RegExp("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
+ return regex.test(val);
+ }
+ return false;
+}
+
+export function fallBackHex(val: string | null): string {
+ if (val) if (colorNameToHex(val)) return colorNameToHex(val) as string;
+ return brandColor;
+}
+
+const BrandColor = ({ val = brandColor }: { val: string | undefined | null }) => {
+ // ensure acceptable hex-code
+ val = isValidHexCode(val) ? (val?.indexOf("#") === 0 ? val : "#" + val) : fallBackHex(val);
useEffect(() => {
document.documentElement.style.setProperty("--brand-color", val);
document.documentElement.style.setProperty("--brand-text-color", getContrastingTextColor(val));
diff --git a/components/Swatch.tsx b/components/Swatch.tsx
new file mode 100644
index 0000000000..89a49af86c
--- /dev/null
+++ b/components/Swatch.tsx
@@ -0,0 +1,26 @@
+import classNames from "@lib/classNames";
+
+export type SwatchProps = {
+ size?: "base" | "sm" | "lg";
+ backgroundColor: string;
+ onClick: () => void;
+};
+
+const Swatch = (props: SwatchProps) => {
+ const { size, backgroundColor, onClick } = props;
+ return (
+
+ );
+};
+
+export default Swatch;
diff --git a/components/ui/colorpicker.tsx b/components/ui/colorpicker.tsx
new file mode 100644
index 0000000000..b00e96790b
--- /dev/null
+++ b/components/ui/colorpicker.tsx
@@ -0,0 +1,100 @@
+import { useCallback, useRef, useState } from "react";
+import { useEffect } from "react";
+import { HexColorInput, HexColorPicker } from "react-colorful";
+
+import { isValidHexCode, fallBackHex } from "@components/CustomBranding";
+import Swatch from "@components/Swatch";
+
+type Handler = (event: MouseEvent | Event) => void;
+function useEventListener<
+ KW extends keyof WindowEventMap,
+ KH extends keyof HTMLElementEventMap,
+ T extends HTMLElement | void = void
+>(
+ eventName: KW | KH,
+ handler: (event: WindowEventMap[KW] | HTMLElementEventMap[KH] | Event) => void,
+ element?: React.RefObject
+) {
+ // Create a ref that stores handler
+ const savedHandler = useRef();
+ useEffect(() => {
+ // Define the listening target
+ const targetElement: T | Window = element?.current || window;
+ if (!(targetElement && targetElement.addEventListener)) {
+ return;
+ }
+ // Update saved handler if necessary
+ if (savedHandler.current !== handler) {
+ savedHandler.current = handler;
+ }
+ // Create event listener that calls handler function stored in ref
+ const eventListener: typeof handler = (event) => {
+ // eslint-disable-next-line no-extra-boolean-cast
+ if (!!savedHandler?.current) {
+ savedHandler.current(event);
+ }
+ };
+ targetElement.addEventListener(eventName, eventListener);
+ // Remove event listener on cleanup
+ return () => {
+ targetElement.removeEventListener(eventName, eventListener);
+ };
+ }, [eventName, element, handler]);
+}
+
+function useOnClickOutside(
+ ref: React.RefObject,
+ handler: Handler,
+ mouseEvent: "mousedown" | "mouseup" = "mousedown"
+): void {
+ useEventListener(mouseEvent, (event) => {
+ const el = ref?.current;
+ // Do nothing if clicking ref's element or descendent elements
+ if (!el || el.contains(event.target as Node)) {
+ return;
+ }
+ handler(event);
+ });
+}
+export type ColorPickerProps = {
+ defaultValue: string;
+ onChange: (text: string) => void;
+};
+
+const ColorPicker = (props: ColorPickerProps) => {
+ const init = !isValidHexCode(props.defaultValue) ? fallBackHex(props.defaultValue) : props.defaultValue;
+ const [color, setColor] = useState(init);
+ const [isOpen, toggle] = useState(false);
+ const popover = useRef() as React.MutableRefObject;
+ const close = useCallback(() => toggle(false), []);
+ useOnClickOutside(popover, close);
+ return (
+
+
toggle(!isOpen)} />
+
+ {isOpen && (
+
+ {
+ setColor(val);
+ props.onChange(val);
+ }}
+ />
+
+ )}
+ {
+ setColor(val);
+ props.onChange(val);
+ }}
+ type="text"
+ />
+
+ );
+};
+
+export default ColorPicker;
diff --git a/package.json b/package.json
index e9a994d6bd..603018892f 100644
--- a/package.json
+++ b/package.json
@@ -85,6 +85,7 @@
"otplib": "^12.0.1",
"qrcode": "^1.5.0",
"react": "^17.0.2",
+ "react-colorful": "^5.5.1",
"react-date-picker": "^8.3.6",
"react-digit-input": "^2.1.0",
"react-dom": "^17.0.2",
@@ -169,4 +170,4 @@
"prisma": {
"seed": "ts-node ./prisma/seed.ts"
}
-}
\ No newline at end of file
+}
diff --git a/pages/settings/profile.tsx b/pages/settings/profile.tsx
index 008dd8baec..72b19ae4d9 100644
--- a/pages/settings/profile.tsx
+++ b/pages/settings/profile.tsx
@@ -30,6 +30,7 @@ import { Alert } from "@components/ui/Alert";
import Avatar from "@components/ui/Avatar";
import Badge from "@components/ui/Badge";
import Button from "@components/ui/Button";
+import ColorPicker from "@components/ui/colorpicker";
type Props = inferSSRProps;
@@ -151,7 +152,6 @@ function SettingsView(props: ComponentProps & { localeProp: str
const emailRef = useRef(null!);
const descriptionRef = useRef(null!);
const avatarRef = useRef(null!);
- const brandColorRef = useRef(null!);
const hideBrandingRef = useRef(null!);
const [selectedTheme, setSelectedTheme] = useState();
const [selectedTimeZone, setSelectedTimeZone] = useState(props.user.timeZone);
@@ -167,6 +167,7 @@ function SettingsView(props: ComponentProps & { localeProp: str
const [imageSrc, setImageSrc] = useState(props.user.avatar || "");
const [hasErrors, setHasErrors] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
+ const [brandColor, setBrandColor] = useState(props.user.brandColor);
useEffect(() => {
if (!props.user.theme) return;
@@ -184,7 +185,7 @@ function SettingsView(props: ComponentProps & { localeProp: str
const enteredEmail = emailRef.current.value;
const enteredDescription = descriptionRef.current.value;
const enteredAvatar = avatarRef.current.value;
- const enteredBrandColor = brandColorRef.current.value;
+ const enteredBrandColor = brandColor;
const enteredTimeZone = typeof selectedTimeZone === "string" ? selectedTimeZone : selectedTimeZone.value;
const enteredWeekStartDay = selectedWeekStartDay.value;
const enteredHideBranding = hideBrandingRef.current.checked;
@@ -402,17 +403,7 @@ function SettingsView(props: ComponentProps & { localeProp: str
-
-
-
+
diff --git a/server/routers/viewer.tsx b/server/routers/viewer.tsx
index 5546494e72..32046212a3 100644
--- a/server/routers/viewer.tsx
+++ b/server/routers/viewer.tsx
@@ -601,6 +601,7 @@ const loggedInViewerRouter = createProtectedRouter()
input: z.object({
username: z.string().optional(),
name: z.string().optional(),
+ email: z.string().optional(),
bio: z.string().optional(),
avatar: z.string().optional(),
timeZone: z.string().optional(),
diff --git a/yarn.lock b/yarn.lock
index 151dbd9194..4a8715ec01 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -9603,6 +9603,11 @@ react-calendar@^3.3.1:
merge-class-names "^1.1.1"
prop-types "^15.6.0"
+react-colorful@^5.5.1:
+ version "5.5.1"
+ resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784"
+ integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg==
+
react-date-picker@^8.3.3:
version "8.3.5"
resolved "https://registry.yarnpkg.com/react-date-picker/-/react-date-picker-8.3.5.tgz#3972db3fc1f37cfd6b09c09bf4988817b4d2be72"