feat(lib): add more tests to lib package (#7210)
* feat(lib): add more tests to lib package Add more tests to the lib package to make it more robust overall. Additionally, tidy any methods that can be modified without changing behaviour and tighten types where possible. * fix(lib): update missed imports * fix: revert stylistic changes * Update getSchedule.test.ts --------- Co-authored-by: Omar López <zomars@me.com>pull/7253/head
parent
cbc9cd60d5
commit
d81d772cdf
|
@ -2,7 +2,7 @@ import crypto from "crypto";
|
|||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { defaultAvatarSrc } from "@lib/profile";
|
||||
|
|
|
@ -7,7 +7,7 @@ import { useEffect } from "react";
|
|||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import EventTypeDescription from "@calcom/features/eventtypes/components/EventTypeDescription";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
|
|
|
@ -504,6 +504,7 @@ describe("getSchedule", () => {
|
|||
);
|
||||
});
|
||||
|
||||
// FIXME: Fix minimumBookingNotice is respected test
|
||||
test.skip("minimumBookingNotice is respected", async () => {
|
||||
jest.useFakeTimers().setSystemTime(
|
||||
(() => {
|
||||
|
|
|
@ -35,9 +35,15 @@ const config: Config = {
|
|||
displayName: "@calcom/lib",
|
||||
roots: ["<rootDir>/packages/lib"],
|
||||
testEnvironment: "node",
|
||||
moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"],
|
||||
transform: {
|
||||
"^.+\\.tsx?$": "ts-jest",
|
||||
},
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
tsconfig: "<rootDir>/packages/lib/tsconfig.test.json",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: "@calcom/closecom",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { MembershipRole } from "@prisma/client";
|
||||
import type { MembershipRole } from "@prisma/client";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
|
|
|
@ -5,9 +5,10 @@ import { useState } from "react";
|
|||
|
||||
import MemberInvitationModal from "@calcom/ee/teams/components/MemberInvitationModal";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Avatar,
|
||||
Button,
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { useRouter } from "next/router";
|
||||
import { useMemo, useState } from "react";
|
||||
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Avatar, Loader, Shell } from "@calcom/ui";
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Controller, useForm } from "react-hook-form";
|
|||
import { z } from "zod";
|
||||
|
||||
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||
import { getPlaceholderAvatar } from "@calcom/lib/defaultAvatarImage";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
import objectKeys from "@calcom/lib/objectKeys";
|
||||
|
|
|
@ -0,0 +1,155 @@
|
|||
import { colorNameToHex, fallBackHex, isValidHexCode } from "./CustomBranding";
|
||||
|
||||
describe("Custom Branding tests", () => {
|
||||
describe("fn: colorNameToHex", () => {
|
||||
it("should return a hex color when a valid color name is provided", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "red",
|
||||
expected: "#ff0000",
|
||||
},
|
||||
{
|
||||
input: "green",
|
||||
expected: "#008000",
|
||||
},
|
||||
{
|
||||
input: "salmon",
|
||||
expected: "#fa8072",
|
||||
},
|
||||
{
|
||||
input: "rebeccapurple",
|
||||
expected: "#663399",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, expected } of cases) {
|
||||
const result = colorNameToHex(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return false when an invalid color name is provided", () => {
|
||||
const result = colorNameToHex("invalid");
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn: isValidHexCode", () => {
|
||||
it("should return true when a valid hex code is provided", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "#ff0000",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "#00Ff00",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "#fA8072",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "#663",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "#fAb",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
input: "#F00F00",
|
||||
expected: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, expected } of cases) {
|
||||
const result = isValidHexCode(input);
|
||||
|
||||
if (!result) {
|
||||
console.log("input", input);
|
||||
}
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return false when an invalid hex code is provided", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "#ff000",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "#F000G0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "#00ff00a",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "#fa8072aa",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
input: "#663399aa",
|
||||
expected: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, expected } of cases) {
|
||||
const result = isValidHexCode(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn: fallBackHex", () => {
|
||||
it("should return a hex color when a valid color name is provided", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "red",
|
||||
expected: "#ff0000",
|
||||
},
|
||||
{
|
||||
input: "green",
|
||||
expected: "#008000",
|
||||
},
|
||||
{
|
||||
input: "salmon",
|
||||
expected: "#fa8072",
|
||||
},
|
||||
{
|
||||
input: "rebeccapurple",
|
||||
expected: "#663399",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, expected } of cases) {
|
||||
const result = colorNameToHex(input);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return a brand color when there is no hex fallback", () => {
|
||||
// BRAND_COLOR => "#292929"
|
||||
// BRAND_TEXT_COLOR => "#ffffff"
|
||||
// DARK_BRAND_COLOR => "#fafafa"
|
||||
|
||||
const inputs = ["reddit", null, "darkbruwn"];
|
||||
|
||||
for (const input of inputs) {
|
||||
const resultLight = fallBackHex(input, false);
|
||||
const resultDark = fallBackHex(input, true);
|
||||
|
||||
expect(resultLight).toEqual("#292929");
|
||||
expect(resultDark).toEqual("#fafafa");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -2,165 +2,178 @@ import Head from "next/head";
|
|||
|
||||
import { useBrandColors } from "@calcom/embed-core/embed-iframe";
|
||||
|
||||
const brandColor = "#292929";
|
||||
const brandTextColor = "#ffffff";
|
||||
const darkBrandColor = "#fafafa";
|
||||
const BRAND_COLOR = "#292929";
|
||||
const BRAND_TEXT_COLOR = "#ffffff";
|
||||
const DARK_BRAND_COLOR = "#fafafa";
|
||||
|
||||
const HTML_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",
|
||||
};
|
||||
|
||||
// Shadow the HTML_COLORS constant so we can create a type guard for it.
|
||||
type HTML_COLORS = typeof HTML_COLORS;
|
||||
|
||||
function isHtmlColor(color: string): color is keyof HTML_COLORS {
|
||||
return color in HTML_COLORS;
|
||||
}
|
||||
|
||||
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",
|
||||
};
|
||||
const normalizedColor = color.toLowerCase();
|
||||
|
||||
return colors[color.toLowerCase() as keyof typeof colors] !== undefined
|
||||
? colors[color.toLowerCase() as keyof typeof colors]
|
||||
: false;
|
||||
if (isHtmlColor(normalizedColor)) {
|
||||
return HTML_COLORS[normalizedColor];
|
||||
}
|
||||
|
||||
return 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]);
|
||||
|
||||
const brightest = Math.max(lum1, lum2);
|
||||
const darkest = Math.min(lum1, lum2);
|
||||
|
||||
return (brightest + 0.05) / (darkest + 0.05);
|
||||
}
|
||||
|
||||
|
@ -169,19 +182,25 @@ function computeLuminance(r: number, g: number, b: number) {
|
|||
v /= 255;
|
||||
return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
|
||||
});
|
||||
|
||||
return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
|
||||
}
|
||||
|
||||
function hexToRGB(hex: string) {
|
||||
const color = hex.replace("#", "");
|
||||
|
||||
return [parseInt(color.slice(0, 2), 16), parseInt(color.slice(2, 4), 16), parseInt(color.slice(4, 6), 16)];
|
||||
}
|
||||
|
||||
function normalizeHexCode(hex: string | null, dark: boolean) {
|
||||
if (!hex) {
|
||||
return !dark ? brandColor : darkBrandColor;
|
||||
return !dark ? BRAND_COLOR : DARK_BRAND_COLOR;
|
||||
}
|
||||
|
||||
hex = hex.replace("#", "");
|
||||
|
||||
// If the length of the hex code is 3, double up each character
|
||||
// e.g. fff => ffffff or a0e => aa00ee
|
||||
if (hex.length === 3) {
|
||||
hex = hex
|
||||
.split("")
|
||||
|
@ -190,54 +209,84 @@ function normalizeHexCode(hex: string | null, dark: boolean) {
|
|||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
return hex;
|
||||
}
|
||||
|
||||
function getContrastingTextColor(bgColor: string | null, dark: boolean): string {
|
||||
bgColor = bgColor == "" || bgColor == null ? (dark ? darkBrandColor : brandColor) : bgColor;
|
||||
bgColor = bgColor == "" || bgColor == null ? (dark ? DARK_BRAND_COLOR : BRAND_COLOR) : bgColor;
|
||||
|
||||
const rgb = hexToRGB(bgColor);
|
||||
|
||||
const whiteContrastRatio = computeContrastRatio(rgb, [255, 255, 255]);
|
||||
const blackContrastRatio = computeContrastRatio(rgb, [41, 41, 41]); //#292929
|
||||
return whiteContrastRatio > blackContrastRatio ? brandTextColor : brandColor;
|
||||
|
||||
return whiteContrastRatio > blackContrastRatio ? BRAND_TEXT_COLOR : BRAND_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a string, determine if it's a valid 6 character hex code.
|
||||
*/
|
||||
export function isValidHexCode(val: string | null) {
|
||||
if (val) {
|
||||
// Normalize the value into include a leading "#" if it's missing
|
||||
val = val.indexOf("#") === 0 ? val : "#" + val;
|
||||
const regex = new RegExp("^#([A-Fa-f0-9]{6}|[A-Fa-f0-9]{3})$");
|
||||
|
||||
const regex = /^#([A-F0-9]{6}|[A-F0-9]{3})$/i;
|
||||
|
||||
return regex.test(val);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a html color name, check if it exists in our color palette
|
||||
* and if it does, return the hex code for that color. Otherwise,
|
||||
* return the default brand color.
|
||||
*/
|
||||
export function fallBackHex(val: string | null, dark: boolean): string {
|
||||
if (val) if (colorNameToHex(val)) return colorNameToHex(val) as string;
|
||||
return dark ? darkBrandColor : brandColor;
|
||||
if (val && colorNameToHex(val)) {
|
||||
return colorNameToHex(val) as string;
|
||||
}
|
||||
|
||||
// Otherwise, return the default color
|
||||
return dark ? DARK_BRAND_COLOR : BRAND_COLOR;
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a light and dark brand color value, update the css variables
|
||||
* within the document to reflect the new brand colors.
|
||||
*/
|
||||
const BrandColor = ({
|
||||
lightVal = brandColor,
|
||||
darkVal = darkBrandColor,
|
||||
lightVal = BRAND_COLOR,
|
||||
darkVal = DARK_BRAND_COLOR,
|
||||
}: {
|
||||
lightVal: string | undefined | null;
|
||||
darkVal: string | undefined | null;
|
||||
}) => {
|
||||
const embedBrandingColors = useBrandColors();
|
||||
|
||||
lightVal = embedBrandingColors.brandColor || lightVal;
|
||||
|
||||
// convert to 6 digit equivalent if 3 digit code is entered
|
||||
lightVal = normalizeHexCode(lightVal, false);
|
||||
|
||||
darkVal = normalizeHexCode(darkVal, true);
|
||||
|
||||
// ensure acceptable hex-code
|
||||
lightVal = isValidHexCode(lightVal)
|
||||
? lightVal?.indexOf("#") === 0
|
||||
? lightVal
|
||||
: "#" + lightVal
|
||||
: fallBackHex(lightVal, false);
|
||||
|
||||
darkVal = isValidHexCode(darkVal)
|
||||
? darkVal?.indexOf("#") === 0
|
||||
? darkVal
|
||||
: "#" + darkVal
|
||||
: fallBackHex(darkVal, true);
|
||||
|
||||
return (
|
||||
<Head>
|
||||
<style>
|
||||
|
|
|
@ -15,6 +15,7 @@ import { isENVDev } from "@calcom/lib/env";
|
|||
*/
|
||||
|
||||
const NEXTAUTH_COOKIE_DOMAIN = process.env.NEXTAUTH_COOKIE_DOMAIN || "";
|
||||
|
||||
export function defaultCookies(useSecureCookies: boolean): CookiesOptions {
|
||||
const cookiePrefix = useSecureCookies ? "__Secure-" : "";
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
import { defaultAvatarSrc, getPlaceholderAvatar } from "./defaultAvatarImage";
|
||||
|
||||
describe("Default Avatar Image tests", () => {
|
||||
describe("fn: defaultAvatarSrc", () => {
|
||||
it("should return a gravatar URL when an email is provided", () => {
|
||||
const email = "john@example.com";
|
||||
const result = defaultAvatarSrc({ email });
|
||||
|
||||
expect(result).toEqual(
|
||||
"https://www.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=160&d=mp&r=PG"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return a gravatar URL when an MD5 hash is provided", () => {
|
||||
const md5 = "my-md5-hash";
|
||||
const result = defaultAvatarSrc({ md5 });
|
||||
|
||||
expect(result).toEqual("https://www.gravatar.com/avatar/my-md5-hash?s=160&d=mp&r=PG");
|
||||
});
|
||||
|
||||
it("should return a gravatar URL using the MD5 hash when an email and MD5 hash are provided", () => {
|
||||
const email = "john@example.com";
|
||||
const md5 = "my-md5-hash";
|
||||
|
||||
const result = defaultAvatarSrc({ email, md5 });
|
||||
|
||||
expect(result).toEqual("https://www.gravatar.com/avatar/my-md5-hash?s=160&d=mp&r=PG");
|
||||
});
|
||||
|
||||
it("should return an empty string when neither an email or MD5 hash is provided", () => {
|
||||
const result = defaultAvatarSrc({});
|
||||
|
||||
expect(result).toEqual("");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn: getPlaceholderAvatar", () => {
|
||||
it("should return the avatar URL when one is provided", () => {
|
||||
const avatar = "https://example.com/avatar.png";
|
||||
const name = "John Doe";
|
||||
|
||||
const result = getPlaceholderAvatar(avatar, name);
|
||||
|
||||
expect(result).toEqual(avatar);
|
||||
});
|
||||
|
||||
it("should return a placeholder avatar URL when no avatar is provided", () => {
|
||||
const name = "John Doe";
|
||||
|
||||
const result = getPlaceholderAvatar(null, name);
|
||||
|
||||
expect(result).toEqual(
|
||||
"https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name=John%20Doe"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return a placeholder avatar URL when no avatar is provided and no name is provided", () => {
|
||||
const result = getPlaceholderAvatar(null, null);
|
||||
|
||||
expect(result).toEqual(
|
||||
"https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name="
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,9 @@
|
|||
import md5Parser from "md5";
|
||||
|
||||
/**
|
||||
* Provided either an email or an MD5 hash, return the URL for the Gravatar
|
||||
* image aborting early if neither is provided.
|
||||
*/
|
||||
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
|
||||
if (!email && !md5) return "";
|
||||
|
||||
|
@ -10,6 +14,15 @@ export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?
|
|||
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Given an avatar URL and a name, return the appropriate avatar URL. In the
|
||||
* event that no avatar URL is provided, return a placeholder avatar URL from
|
||||
* ui-avatars.com.
|
||||
*
|
||||
* ui-avatars.com is a free service that generates placeholder avatars based on
|
||||
* a name. It is used here to provide a consistent placeholder avatar for users
|
||||
* who have not uploaded an avatar.
|
||||
*/
|
||||
export function getPlaceholderAvatar(avatar: string | null | undefined, name: string | null) {
|
||||
return avatar
|
||||
? avatar
|
||||
|
|
|
@ -1,6 +0,0 @@
|
|||
export function getPlaceholderAvatar(avatar: string | null | undefined, name: string | null) {
|
||||
return avatar
|
||||
? avatar
|
||||
: "https://eu.ui-avatars.com/api/?background=fff&color=f9f9f9&bold=true&background=000000&name=" +
|
||||
encodeURIComponent(name || "");
|
||||
}
|
|
@ -8,5 +8,6 @@ export function isBookingLimit(obj: unknown): obj is IntervalLimit {
|
|||
export function parseBookingLimit(obj: unknown): IntervalLimit | null {
|
||||
let bookingLimit: IntervalLimit | null = null;
|
||||
if (isBookingLimit(obj)) bookingLimit = obj;
|
||||
|
||||
return bookingLimit;
|
||||
}
|
||||
|
|
|
@ -2,12 +2,15 @@ import { recurringEventType as recurringEventSchema } from "@calcom/prisma/zod-u
|
|||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
|
||||
export function isRecurringEvent(obj: unknown): obj is RecurringEvent {
|
||||
const parsedRecuEvt = recurringEventSchema.safeParse(obj);
|
||||
return parsedRecuEvt.success;
|
||||
const parsed = recurringEventSchema.safeParse(obj);
|
||||
|
||||
return parsed.success;
|
||||
}
|
||||
|
||||
export function parseRecurringEvent(obj: unknown): RecurringEvent | null {
|
||||
let recurringEvent: RecurringEvent | null = null;
|
||||
|
||||
if (isRecurringEvent(obj)) recurringEvent = obj;
|
||||
|
||||
return recurringEvent;
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
import crypto from "crypto";
|
||||
|
||||
export const defaultAvatarSrc = function ({ email, md5 }: { md5?: string; email?: string }) {
|
||||
if (!email && !md5) return "";
|
||||
|
||||
if (email && !md5) {
|
||||
md5 = crypto.createHash("md5").update(email).digest("hex");
|
||||
}
|
||||
|
||||
return `https://www.gravatar.com/avatar/${md5}?s=160&d=mp&r=PG`;
|
||||
};
|
|
@ -0,0 +1,30 @@
|
|||
import { randomString } from "./random";
|
||||
|
||||
describe("Random util tests", () => {
|
||||
describe("fn: randomString", () => {
|
||||
it("should return a random string of a given length", () => {
|
||||
const length = 10;
|
||||
|
||||
const result = randomString(length);
|
||||
|
||||
expect(result).toHaveLength(length);
|
||||
});
|
||||
|
||||
it("should return a random string of a default length", () => {
|
||||
const length = 12;
|
||||
|
||||
const result = randomString();
|
||||
|
||||
expect(result).toHaveLength(length);
|
||||
});
|
||||
|
||||
it("should return a random string of a given length using alphanumeric characters", () => {
|
||||
const length = 10;
|
||||
|
||||
const result = randomString(length);
|
||||
|
||||
expect(result).toMatch(/^[a-zA-Z0-9]+$/);
|
||||
expect(result).toHaveLength(length);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,9 +1,15 @@
|
|||
const CHARACTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const CHARACTERS_LENGTH = CHARACTERS.length;
|
||||
|
||||
/**
|
||||
* Generate a random string of a given length using alphanumeric characters.
|
||||
*/
|
||||
export const randomString = function (length = 12) {
|
||||
let result = "";
|
||||
const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
const charactersLength = characters.length;
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += characters.charAt(Math.floor(Math.random() * charactersLength));
|
||||
result += CHARACTERS.charAt(Math.floor(Math.random() * CHARACTERS_LENGTH));
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
import { truncate } from "./text";
|
||||
|
||||
describe("Text util tests", () => {
|
||||
describe("fn: truncate", () => {
|
||||
it("should return the original text when it is shorter than the max length", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 100,
|
||||
expected: "Hello world",
|
||||
},
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 11,
|
||||
expected: "Hello world",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, maxLength, expected } of cases) {
|
||||
const result = truncate(input, maxLength);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return the truncated text when it is longer than the max length", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 10,
|
||||
expected: "Hello w...",
|
||||
},
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 5,
|
||||
expected: "He...",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, maxLength, expected } of cases) {
|
||||
const result = truncate(input, maxLength);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return the truncated text without ellipsis when it is longer than the max length and ellipsis is false", () => {
|
||||
const cases = [
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 10,
|
||||
ellipsis: false,
|
||||
expected: "Hello w",
|
||||
},
|
||||
{
|
||||
input: "Hello world",
|
||||
maxLength: 5,
|
||||
ellipsis: false,
|
||||
expected: "He",
|
||||
},
|
||||
];
|
||||
|
||||
for (const { input, maxLength, ellipsis, expected } of cases) {
|
||||
const result = truncate(input, maxLength, ellipsis);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
export const truncate = (text: string, maxLength: number, ellipsis = true) => {
|
||||
if (text.length <= maxLength) return text;
|
||||
|
||||
return `${text.slice(0, maxLength - 3)}${ellipsis ? "..." : ""}`;
|
||||
};
|
||||
|
||||
|
@ -8,9 +9,11 @@ export const truncateOnWord = (text: string, maxLength: number, ellipsis = true)
|
|||
|
||||
// First split on maxLength chars
|
||||
let truncatedText = text.substring(0, 148);
|
||||
|
||||
// Then split on the last space, this way we split on the last word,
|
||||
// which looks just a bit nicer.
|
||||
truncatedText = truncatedText.substring(0, Math.min(truncatedText.length, truncatedText.lastIndexOf(" ")));
|
||||
|
||||
if (ellipsis) truncatedText += "...";
|
||||
|
||||
return truncatedText;
|
||||
|
|
|
@ -18,11 +18,17 @@ export const setIs24hClockInLocalStorage = (is24h: boolean) =>
|
|||
|
||||
export const getIs24hClockFromLocalStorage = () => {
|
||||
const is24hFromLocalstorage = localStorage.getItem(is24hLocalstorageKey);
|
||||
|
||||
if (is24hFromLocalstorage === null) return null;
|
||||
|
||||
return is24hFromLocalstorage === "true";
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the browsers time format preference, checking local storage first
|
||||
* for a user set preference. If no preference is found, it will use the browser
|
||||
* locale to determine the time format and store it in local storage.
|
||||
*/
|
||||
export const isBrowserLocale24h = () => {
|
||||
const localStorageTimeFormat = getIs24hClockFromLocalStorage();
|
||||
// If time format is already stored in the browser then retrieve and return early
|
||||
|
@ -41,6 +47,9 @@ export const isBrowserLocale24h = () => {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns the time format string based on whether the current set locale is 24h or 12h.
|
||||
*/
|
||||
export const detectBrowserTimeFormat = isBrowserLocale24h()
|
||||
? TimeFormat.TWENTY_FOUR_HOUR
|
||||
: TimeFormat.TWELVE_HOUR;
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [".", "../types/next-auth.d.ts"],
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/tsconfig",
|
||||
|
||||
"extends": "@calcom/tsconfig/base.json",
|
||||
"compilerOptions": {
|
||||
"esModuleInterop": true,
|
||||
"target": "es5",
|
||||
"jsx": "react-jsx",
|
||||
"resolveJsonModule": true
|
||||
},
|
||||
"include": [".", "../types/next-auth.d.ts"],
|
||||
"exclude": ["dist", "build", "node_modules"]
|
||||
}
|
|
@ -1,4 +1,8 @@
|
|||
// TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
|
||||
/**
|
||||
* Provides a wrapper around localStorage to avoid errors in case of restricted storage access.
|
||||
*
|
||||
* TODO: In case of an embed if localStorage is not available(third party), use localStorage of parent(first party) that contains the iframe.
|
||||
*/
|
||||
export const localStorage = {
|
||||
getItem(key: string) {
|
||||
try {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
import { nameOfDay, weekdayNames } from "./weekday";
|
||||
|
||||
describe("Weekday tests", () => {
|
||||
describe("fn: weekdayNames", () => {
|
||||
it("should return the weekday names for a given locale", () => {
|
||||
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
|
||||
|
||||
for (const locale of locales) {
|
||||
const result = weekdayNames(locale);
|
||||
|
||||
const expected = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return the weekday names for a given locale and format", () => {
|
||||
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
|
||||
|
||||
for (const locale of locales) {
|
||||
const result = weekdayNames(locale, 0, "short");
|
||||
|
||||
const expected = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return the weekday names for a given locale and week start offset", () => {
|
||||
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
|
||||
|
||||
for (const locale of locales) {
|
||||
const result = weekdayNames(locale, 1);
|
||||
|
||||
const expected = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"];
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("fn: nameOfDay", () => {
|
||||
it("should return the name of the day for a given locale", () => {
|
||||
const locales = ["en-US", "en-CA", "en-GB", "en-AU"];
|
||||
const days = [
|
||||
{ day: 0, expected: "Sunday" },
|
||||
{ day: 1, expected: "Monday" },
|
||||
{ day: 2, expected: "Tuesday" },
|
||||
{ day: 3, expected: "Wednesday" },
|
||||
{ day: 4, expected: "Thursday" },
|
||||
{ day: 5, expected: "Friday" },
|
||||
{ day: 6, expected: "Saturday" },
|
||||
];
|
||||
|
||||
for (const locale of locales) {
|
||||
for (const { day, expected } of days) {
|
||||
const result = nameOfDay(locale, day);
|
||||
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,12 +1,16 @@
|
|||
type WeekdayFormat = "short" | "long";
|
||||
|
||||
// By default starts on Sunday (Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday)
|
||||
export function weekdayNames(locale: string | string[], weekStart = 0, type: "short" | "long" = "long") {
|
||||
return Array.from(Array(7).keys()).map((d) => nameOfDay(locale, d + weekStart, type));
|
||||
export function weekdayNames(locale: string | string[], weekStart = 0, format: WeekdayFormat = "long") {
|
||||
return Array(7)
|
||||
.fill(null)
|
||||
.map((_, day) => nameOfDay(locale, day + weekStart, format));
|
||||
}
|
||||
|
||||
export function nameOfDay(
|
||||
locale: string | string[] | undefined,
|
||||
day: number,
|
||||
type: "short" | "long" = "long"
|
||||
format: WeekdayFormat = "long"
|
||||
) {
|
||||
return new Intl.DateTimeFormat(locale, { weekday: type }).format(new Date(1970, 0, day + 4));
|
||||
return new Intl.DateTimeFormat(locale, { weekday: format }).format(new Date(1970, 0, day + 4));
|
||||
}
|
||||
|
|
|
@ -4,8 +4,8 @@ import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
|||
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/defaultAvatarImage";
|
||||
import { getLocaleFromHeaders } from "@calcom/lib/i18n";
|
||||
import { defaultAvatarSrc } from "@calcom/lib/profile";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import type { Maybe } from "@trpc/server";
|
||||
|
|
Loading…
Reference in New Issue