Brand color without correct hex value compatibility (#1737)

* enabled fallback support and og-brand-color fix

* --added react-colorful package

* added colorpicker component and added it to settings/profile

* lint fix

* typo fix in server/viewer

* clean-up

* improved colorpicker component

* added swatch component and integrated with brand color picker

* improved ux for color picker swatch

* added colorname integration

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
pull/1715/head
Syed Ali Shahbaz 2022-02-08 21:43:42 +05:30 committed by GitHub
parent 447def5849
commit 522875da74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 310 additions and 17 deletions

View File

@ -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));

26
components/Swatch.tsx Normal file
View File

@ -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 (
<div className="p-1 border-2 border-gray-200 shadow-sm">
<div
onClick={onClick}
style={{ backgroundColor }}
className={classNames(
"cursor-pointer",
size === "sm" && "w-6 h-6 rounded-sm",
size === "base" && "w-16 h-16 rounded-sm",
size === "lg" && "w-24 h-24 rounded-sm"
)}></div>
</div>
);
};
export default Swatch;

View File

@ -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<T>
) {
// Create a ref that stores handler
const savedHandler = useRef<typeof handler>();
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<T extends HTMLElement = HTMLElement>(
ref: React.RefObject<T>,
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<HTMLInputElement>;
const close = useCallback(() => toggle(false), []);
useOnClickOutside(popover, close);
return (
<div className="relative flex items-center justify-center mt-1">
<Swatch size="sm" backgroundColor={color} onClick={() => toggle(!isOpen)} />
{isOpen && (
<div className="popover" ref={popover}>
<HexColorPicker
className="!w-32 !h-32 !absolute !top-10 !left-0 !z-10"
color={color}
onChange={(val) => {
setColor(val);
props.onChange(val);
}}
/>
</div>
)}
<HexColorInput
className="block w-full px-3 py-2 ml-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-800 focus:border-neutral-800 sm:text-sm"
color={color}
onChange={(val) => {
setColor(val);
props.onChange(val);
}}
type="text"
/>
</div>
);
};
export default ColorPicker;

View File

@ -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"
}
}
}

View File

@ -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<typeof getServerSideProps>;
@ -151,7 +152,6 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
const emailRef = useRef<HTMLInputElement>(null!);
const descriptionRef = useRef<HTMLTextAreaElement>(null!);
const avatarRef = useRef<HTMLInputElement>(null!);
const brandColorRef = useRef<HTMLInputElement>(null!);
const hideBrandingRef = useRef<HTMLInputElement>(null!);
const [selectedTheme, setSelectedTheme] = useState<typeof themeOptions[number] | undefined>();
const [selectedTimeZone, setSelectedTimeZone] = useState<ITimezone>(props.user.timeZone);
@ -167,6 +167,7 @@ function SettingsView(props: ComponentProps<typeof Settings> & { localeProp: str
const [imageSrc, setImageSrc] = useState<string>(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<typeof Settings> & { 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<typeof Settings> & { localeProp: str
<label htmlFor="brandColor" className="block text-sm font-medium text-gray-700">
{t("brand_color")}
</label>
<div className="flex mt-1">
<input
ref={brandColorRef}
type="text"
name="brandColor"
id="brandColor"
placeholder="#hex-code"
className="block w-full px-3 py-2 mt-1 border border-gray-300 rounded-sm shadow-sm focus:outline-none focus:ring-neutral-800 focus:border-neutral-800 sm:text-sm"
defaultValue={props.user.brandColor}
/>
</div>
<ColorPicker defaultValue={props.user.brandColor} onChange={setBrandColor} />
<hr className="mt-6" />
</div>
<div>

View File

@ -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(),

View File

@ -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"