diff --git a/apps/api/lib/validations/user.ts b/apps/api/lib/validations/user.ts index 98e876c4fe..2bccf2f961 100644 --- a/apps/api/lib/validations/user.ts +++ b/apps/api/lib/validations/user.ts @@ -74,6 +74,7 @@ export const schemaUserBaseBodyParams = User.pick({ timeZone: true, weekStart: true, theme: true, + appTheme: true, defaultScheduleId: true, locale: true, hideBranding: true, @@ -100,6 +101,7 @@ const schemaUserEditParams = z.object({ hideBranding: z.boolean().optional(), timeZone: timeZone.optional(), theme: z.nativeEnum(theme).optional().nullable(), + appTheme: z.nativeEnum(theme).optional().nullable(), timeFormat: z.nativeEnum(timeFormat).optional(), defaultScheduleId: z .number() @@ -122,6 +124,7 @@ const schemaUserCreateParams = z.object({ hideBranding: z.boolean().optional(), timeZone: timeZone.optional(), theme: z.nativeEnum(theme).optional().nullable(), + appTheme: z.nativeEnum(theme).optional().nullable(), timeFormat: z.nativeEnum(timeFormat).optional(), defaultScheduleId: z .number() @@ -159,6 +162,7 @@ export const schemaUserReadPublic = User.pick({ weekStart: true, endTime: true, bufferTime: true, + appTheme: true, theme: true, defaultScheduleId: true, locale: true, diff --git a/apps/web/lib/app-providers-app-dir.tsx b/apps/web/lib/app-providers-app-dir.tsx index 006145761a..029fed2c0a 100644 --- a/apps/web/lib/app-providers-app-dir.tsx +++ b/apps/web/lib/app-providers-app-dir.tsx @@ -113,7 +113,7 @@ const enum ThemeSupport { // e.g. Login Page None = "none", // Entire App except Booking Pages - App = "systemOnly", + App = "userConfigured", // Booking Pages(including Routing Forms) Booking = "userConfigured", } diff --git a/apps/web/lib/app-providers.tsx b/apps/web/lib/app-providers.tsx index 2e3e8c1086..1a478cff52 100644 --- a/apps/web/lib/app-providers.tsx +++ b/apps/web/lib/app-providers.tsx @@ -127,9 +127,9 @@ const enum ThemeSupport { // e.g. Login Page None = "none", // Entire App except Booking Pages - App = "systemOnly", + App = "appConfigured", // Booking Pages(including Routing Forms) - Booking = "userConfigured", + Booking = "bookingConfigured", } type CalcomThemeProps = PropsWithChildren< @@ -144,8 +144,10 @@ const CalcomThemeProvider = (props: CalcomThemeProps) => { const embedNamespace = getEmbedNamespace(props.router.query); const isEmbedMode = typeof embedNamespace === "string"; + const themeProviderProps = getThemeProviderProps({ props, isEmbedMode, embedNamespace }); + return ( - + {/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */} {/* color-scheme makes background:transparent not work in iframe which is required by embed. */} {typeof window !== "undefined" && !isEmbedMode && ( diff --git a/apps/web/pages/settings/my-account/appearance.tsx b/apps/web/pages/settings/my-account/appearance.tsx index 2bc76c2126..5a3965eaf8 100644 --- a/apps/web/pages/settings/my-account/appearance.tsx +++ b/apps/web/pages/settings/my-account/appearance.tsx @@ -11,8 +11,10 @@ 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 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"; @@ -29,6 +31,7 @@ import { SkeletonText, SettingsToggle, UpgradeTeamsBadge, + useCalcomTheme, } from "@calcom/ui"; import PageWrapper from "@components/PageWrapper"; @@ -41,10 +44,10 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
-
- - - +
+
+
+
@@ -62,6 +65,26 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st ); }; +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, @@ -77,6 +100,22 @@ const AppearanceView = ({ 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: { @@ -131,6 +170,7 @@ const AppearanceView = ({ resetBrandColorsThemeReset({ brandColor: data.brandColor, darkBrandColor: data.darkBrandColor }); resetBookerLayoutThemeReset({ metadata: data.metadata }); resetUserThemeReset({ theme: data.theme }); + resetUserAppThemeReset({ appTheme: data.appTheme }); }, onError: (error) => { if (error.message) { @@ -147,6 +187,56 @@ const AppearanceView = ({ return (
+
+
+

{t("app_theme")}

+

{t("app_theme_applies_note")}

+
+
+
{ + mutation.mutate({ + appTheme: values.appTheme ?? null, + }); + }}> +
+ + + +
+ + + +
+

{t("theme")}

diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json index b8cbed57df..5018faf54f 100644 --- a/apps/web/public/static/locales/en/common.json +++ b/apps/web/public/static/locales/en/common.json @@ -1473,8 +1473,10 @@ "route_to_the_right_person": "Route to the right person based on the answers to your form", "form_description": "Create your form to route a booker", "copy_link_to_form": "Copy link to form", - "theme": "Theme", + "theme": "Booking Page Theme", "theme_applies_note": "This only applies to your public booking pages", + "app_theme": "Dashboard Theme", + "app_theme_applies_note": "This only applies to your logged in dashboard", "theme_system": "System default", "add_a_team": "Add a team", "add_webhook_description": "Receive meeting data in real-time when something happens in {{appName}}", diff --git a/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts b/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts index 1dc6580b2d..5057c206d2 100644 --- a/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts +++ b/apps/web/test/utils/bookingScenario/getSampleUserInSession.ts @@ -37,6 +37,7 @@ export const getSampleUserInSession = function () { disableImpersonation: false, organizationId: null, theme: "", + appTheme: "", createdDate: new Date(), trialEndsAt: new Date(), completedOnboarding: false, diff --git a/packages/features/settings/ThemeLabel.tsx b/packages/features/settings/ThemeLabel.tsx index 3793b77e0b..7d5bafa02b 100644 --- a/packages/features/settings/ThemeLabel.tsx +++ b/packages/features/settings/ThemeLabel.tsx @@ -4,23 +4,24 @@ interface ThemeLabelProps { label: string; defaultChecked?: boolean; register: any; + fieldName?: string; } export default function ThemeLabel(props: ThemeLabelProps) { - const { variant, label, value, defaultChecked, register } = props; + const { variant, label, value, defaultChecked, register, fieldName = "theme" } = props; return (