"use client"; import { useSession } from "next-auth/react"; import { useState } from "react"; import { Controller, useForm } from "react-hook-form"; import type { z } from "zod"; import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector"; import SectionBottomActions from "@calcom/features/settings/SectionBottomActions"; import ThemeLabel from "@calcom/features/settings/ThemeLabel"; import { APP_NAME } from "@calcom/lib/constants"; import { DEFAULT_LIGHT_BRAND_COLOR, DEFAULT_DARK_BRAND_COLOR } from "@calcom/lib/constants"; import { checkWCAGContrastColor } from "@calcom/lib/getBrandColours"; import useGetBrandingColours from "@calcom/lib/getBrandColours"; import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import useTheme from "@calcom/lib/hooks/useTheme"; import { validateBookerLayouts } from "@calcom/lib/validateBookerLayouts"; import type { userMetadata } from "@calcom/prisma/zod-utils"; import { trpc } from "@calcom/trpc/react"; import type { RouterOutputs } from "@calcom/trpc/react"; import { Alert, Button, ColorPicker, Form, showToast, SkeletonButton, SkeletonContainer, SkeletonText, SettingsToggle, UpgradeTeamsBadge, useCalcomTheme, } from "@calcom/ui"; const SkeletonLoader = () => { return ( ); }; const useBrandColors = ( currentTheme: string | null, { brandColor, darkBrandColor, }: { brandColor?: string | null; darkBrandColor?: string | null; } ): void => { const brandTheme = useGetBrandingColours({ lightVal: brandColor, darkVal: darkBrandColor, }); const selectedTheme = currentTheme ? brandTheme[currentTheme as "light" | "dark"] : {}; useCalcomTheme({ root: selectedTheme, }); }; const AppearanceView = ({ user, hasPaidPlan, }: { user: RouterOutputs["viewer"]["me"]; hasPaidPlan: boolean; }) => { const { t } = useLocale(); const utils = trpc.useUtils(); const session = useSession(); const isApartOfOrganization = session.data?.user.org?.id; const [darkModeError, setDarkModeError] = useState(false); const [lightModeError, setLightModeError] = useState(false); const [isCustomBrandColorChecked, setIsCustomBranColorChecked] = useState( user?.brandColor !== DEFAULT_LIGHT_BRAND_COLOR || user?.darkBrandColor !== DEFAULT_DARK_BRAND_COLOR ); const [hideBrandingValue, setHideBrandingValue] = useState(user?.hideBranding ?? false); useTheme(user?.appTheme); useBrandColors(user?.appTheme ?? null, { brandColor: user?.brandColor, darkBrandColor: user?.darkBrandColor, }); const userAppThemeFormMethods = useForm({ defaultValues: { appTheme: user.appTheme, }, }); const { formState: { isSubmitting: isUserAppThemeSubmitting, isDirty: isUserAppThemeDirty }, reset: resetUserAppThemeReset, } = userAppThemeFormMethods; const userThemeFormMethods = useForm({ defaultValues: { theme: user.theme, }, }); const { formState: { isSubmitting: isUserThemeSubmitting, isDirty: isUserThemeDirty }, reset: resetUserThemeReset, } = userThemeFormMethods; const bookerLayoutFormMethods = useForm({ defaultValues: { metadata: user.metadata as z.infer, }, }); const { formState: { isSubmitting: isBookerLayoutFormSubmitting, isDirty: isBookerLayoutFormDirty }, reset: resetBookerLayoutThemeReset, } = bookerLayoutFormMethods; const DEFAULT_BRAND_COLOURS = { light: user.brandColor ?? DEFAULT_LIGHT_BRAND_COLOR, dark: user.darkBrandColor ?? DEFAULT_DARK_BRAND_COLOR, }; const brandColorsFormMethods = useForm({ defaultValues: { brandColor: DEFAULT_BRAND_COLOURS.light, darkBrandColor: DEFAULT_BRAND_COLOURS.dark, }, }); const { formState: { isSubmitting: isBrandColorsFormSubmitting, isDirty: isBrandColorsFormDirty }, reset: resetBrandColorsThemeReset, } = brandColorsFormMethods; const selectedTheme = userThemeFormMethods.watch("theme"); const selectedThemeIsDark = selectedTheme === "dark" || (selectedTheme === "" && typeof document !== "undefined" && document.documentElement.classList.contains("dark")); const mutation = trpc.viewer.updateProfile.useMutation({ onSuccess: async (data) => { await utils.viewer.me.invalidate(); showToast(t("settings_updated_successfully"), "success"); resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor }); resetBookerLayoutThemeReset({ metadata: data.metadata }); resetUserThemeReset({ theme: data.theme }); resetUserAppThemeReset({ appTheme: data.appTheme }); }, onError: (error) => { if (error.message) { showToast(error.message, "error"); } else { showToast(t("error_updating_settings"), "error"); } }, onSettled: async () => { await utils.viewer.me.invalidate(); }, }); return ( {t("app_theme")} {t("app_theme_applies_note")} { if (appTheme === "system") appTheme = null; mutation.mutate({ appTheme, }); }}> {t("update")} {isApartOfOrganization ? null : ( <> {t("theme")} {t("theme_applies_note")} { if (theme === "light" || theme === "dark") { mutation.mutate({ theme, }); return; } mutation.mutate({ theme: null, }); }}> {t("update")} { const layoutError = validateBookerLayouts(values?.metadata?.defaultBookerLayouts || null); if (layoutError) { showToast(t(layoutError), "error"); return; } else { mutation.mutate(values); } }}> { mutation.mutate(values); }}> { setIsCustomBranColorChecked(checked); if (!checked) { mutation.mutate({ brandColor: DEFAULT_LIGHT_BRAND_COLOR, darkBrandColor: DEFAULT_DARK_BRAND_COLOR, }); } }} childrenClassName="lg:ml-0"> ( {t("light_brand_color")} { if (checkWCAGContrastColor("#ffffff", value)) { setLightModeError(false); } else { setLightModeError(true); } brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); }} /> {lightModeError ? ( ) : null} )} /> ( {t("dark_brand_color")} { if (checkWCAGContrastColor("#101010", value)) { setDarkModeError(false); } else { setDarkModeError(true); } brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); }} /> {darkModeError ? ( ) : null} )} /> {t("update")} {/* TODO future PR to preview brandColors */} {/* window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}> Preview */} } onCheckedChange={(checked) => { setHideBrandingValue(checked); mutation.mutate({ hideBranding: checked }); }} switchContainerClassName="mt-6" /> > )} ); }; const AppearancePage = () => { const { data: user, isPending } = trpc.viewer.me.useQuery(); const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan(); if (isPending || isTeamPlanStatusLoading || !user) return ; return ; }; export default AppearancePage;
{t("app_theme")}
{t("app_theme_applies_note")}
{t("theme")}
{t("theme_applies_note")}
{t("light_brand_color")}
{t("dark_brand_color")}