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 { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; 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 { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; import { useLocale } from "@calcom/lib/hooks/useLocale"; 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, Meta, showToast, SkeletonButton, SkeletonContainer, SkeletonText, SettingsToggle, UpgradeTeamsBadge, } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { return (
); }; const AppearanceView = ({ user, hasPaidPlan, }: { user: RouterOutputs["viewer"]["me"]; hasPaidPlan: boolean; }) => { const { t } = useLocale(); const utils = trpc.useContext(); 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); 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 brandColorsFormMethods = useForm({ defaultValues: { brandColor: user.brandColor || DEFAULT_LIGHT_BRAND_COLOR, darkBrandColor: user.darkBrandColor || DEFAULT_DARK_BRAND_COLOR, }, }); 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 }); }, onError: (error) => { if (error.message) { showToast(error.message, "error"); } else { showToast(t("error_updating_settings"), "error"); } }, }); return (

{t("theme")}

{t("theme_applies_note")}

{ mutation.mutate({ // Radio values don't support null as values, therefore we convert an empty string // back to null here. theme: values.theme ?? null, }); }}>
{ 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")}

{ try { checkWCAGContrastColor("#ffffff", value); setLightModeError(false); brandColorsFormMethods.setValue("brandColor", value, { shouldDirty: true }); } catch (err) { setLightModeError(false); } }} /> {lightModeError ? (
) : null}
)} /> (

{t("dark_brand_color")}

{ try { checkWCAGContrastColor("#101010", value); setDarkModeError(false); brandColorsFormMethods.setValue("darkBrandColor", value, { shouldDirty: true }); } catch (err) { setDarkModeError(true); } }} /> {darkModeError ? (
) : null}
)} />
{/* TODO future PR to preview brandColors */} {/* */} } onCheckedChange={(checked) => { setHideBrandingValue(checked); mutation.mutate({ hideBranding: checked }); }} switchContainerClassName="mt-6" />
); }; const AppearanceViewWrapper = () => { const { data: user, isLoading } = trpc.viewer.me.useQuery(); const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan(); const { t } = useLocale(); if (isLoading || isTeamPlanStatusLoading || !user) return ; return ; }; AppearanceViewWrapper.getLayout = getLayout; AppearanceViewWrapper.PageWrapper = PageWrapper; export default AppearanceViewWrapper;