diff --git a/apps/web/pages/settings/my-account/appearance.tsx b/apps/web/pages/settings/my-account/appearance.tsx index aca4049caf..0a665570b7 100644 --- a/apps/web/pages/settings/my-account/appearance.tsx +++ b/apps/web/pages/settings/my-account/appearance.tsx @@ -1,6 +1,7 @@ import { GetServerSidePropsContext } from "next"; import { Controller, useForm } from "react-hook-form"; +import ThemeLabel from "@calcom/features/settings/ThemeLabel"; import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout"; import { APP_NAME } from "@calcom/lib/constants"; import { useHasPaidPlan } from "@calcom/lib/hooks/useHasPaidPlan"; @@ -225,36 +226,3 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }; export default AppearanceView; -interface ThemeLabelProps { - variant: "light" | "dark" | "system"; - value?: "light" | "dark" | null; - label: string; - defaultChecked?: boolean; - register: any; -} - -const ThemeLabel = ({ variant, label, value, defaultChecked, register }: ThemeLabelProps) => { - return ( - - ); -}; diff --git a/apps/web/pages/team/[slug].tsx b/apps/web/pages/team/[slug].tsx index f6de4d2033..1855c3f32d 100644 --- a/apps/web/pages/team/[slug].tsx +++ b/apps/web/pages/team/[slug].tsx @@ -27,7 +27,7 @@ const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true } export type TeamPageProps = inferSSRProps; function TeamPage({ team }: TeamPageProps) { - useTheme(); + useTheme(team.theme); const showMembers = useToggleQuery("members"); const { t } = useLocale(); const isEmbed = useIsEmbed(); diff --git a/apps/web/pages/team/[slug]/[type].tsx b/apps/web/pages/team/[slug]/[type].tsx index 50e7817da7..f07f585ea9 100644 --- a/apps/web/pages/team/[slug]/[type].tsx +++ b/apps/web/pages/team/[slug]/[type].tsx @@ -43,6 +43,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => slug: true, logo: true, hideBranding: true, + brandColor: true, + darkBrandColor: true, + theme: true, eventTypes: { where: { slug: typeParam, @@ -68,6 +71,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => }, }, }, + title: true, availability: true, description: true, @@ -150,10 +154,10 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) => name: team.name || team.slug, slug: team.slug, image: team.logo, - theme: null as string | null, + theme: team.theme, weekStart: "Sunday", - brandColor: "" /* TODO: Add a way to set a brand color for Teams */, - darkBrandColor: "" /* TODO: Add a way to set a brand color for Teams */, + brandColor: team.brandColor, + darkBrandColor: team.darkBrandColor, }, date: dateParam, eventType: eventTypeObject, diff --git a/apps/web/pages/team/[slug]/book.tsx b/apps/web/pages/team/[slug]/book.tsx index 577c86ea1e..1e203d7366 100644 --- a/apps/web/pages/team/[slug]/book.tsx +++ b/apps/web/pages/team/[slug]/book.tsx @@ -67,6 +67,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { slug: true, name: true, logo: true, + theme: true, + brandColor: true, + darkBrandColor: true, }, }, users: { @@ -128,9 +131,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) { // FIXME: This slug is used as username on success page which is wrong. This is correctly set as username for user booking. slug: "team/" + eventTypeObject.slug, image: eventTypeObject.team?.logo || null, - theme: null as string | null /* Teams don't have a theme, and `BookingPage` uses it */, - brandColor: null /* Teams don't have a brandColor, and `BookingPage` uses it */, - darkBrandColor: null /* Teams don't have a darkBrandColor, and `BookingPage` uses it */, eventName: null, }, eventType: eventTypeObject, diff --git a/packages/features/ee/teams/pages/team-appearance-view.tsx b/packages/features/ee/teams/pages/team-appearance-view.tsx index 9e7d686817..453b60ff05 100644 --- a/packages/features/ee/teams/pages/team-appearance-view.tsx +++ b/packages/features/ee/teams/pages/team-appearance-view.tsx @@ -5,13 +5,50 @@ import { Controller, useForm } from "react-hook-form"; import { APP_NAME } from "@calcom/lib/constants"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import { trpc } from "@calcom/trpc/react"; -import { Button, Form, Meta, showToast, Switch } from "@calcom/ui"; +import { + Button, + ColorPicker, + Form, + Meta, + showToast, + SkeletonButton, + SkeletonContainer, + SkeletonText, + Switch, +} from "@calcom/ui"; +import ThemeLabel from "../../../settings/ThemeLabel"; import { getLayout } from "../../../settings/layouts/SettingsLayout"; +const SkeletonLoader = ({ title, description }: { title: string; description: string }) => { + return ( + + +
+
+ + + +
+
+ + +
+ + + + +
+
+ ); +}; + interface TeamAppearanceValues { hideBranding: boolean; hideBookATeamMember: boolean; + brandColor: string; + darkBrandColor: string; + theme: string | null | undefined; } const ProfileView = () => { @@ -29,8 +66,6 @@ const ProfileView = () => { }, }); - const form = useForm(); - const { data: team, isLoading } = trpc.viewer.teams.get.useQuery( { teamId: Number(router.query.id) }, { @@ -40,86 +75,163 @@ const ProfileView = () => { } ); + const form = useForm({ + defaultValues: { + theme: team?.theme, + brandColor: team?.brandColor, + darkBrandColor: team?.darkBrandColor, + hideBranding: team?.hideBranding, + }, + }); + const isAdmin = team && (team.membership.role === MembershipRole.OWNER || team.membership.role === MembershipRole.ADMIN); + if (isLoading) { + return ; + } return ( <> - {!isLoading && ( - <> - {isAdmin ? ( -
{ - if (team) { - mutation.mutate({ - id: team.id, - hideBranding: values.hideBranding, - hideBookATeamMember: values.hideBookATeamMember, - }); - } - }}> -
-
-
- -

- {t("team_disable_cal_branding_description", { appName: APP_NAME })} -

-
-
- ( - { - form.setValue("hideBranding", isChecked); - }} - /> - )} - /> -
-
-
-
- -

{t("hide_book_a_team_member_description")}

-
-
- ( - { - form.setValue("hideBookATeamMember", isChecked); - }} - /> - )} - /> -
-
-
- -
- ) : ( -
- {t("only_owner_change")} + {isAdmin ? ( +
{ + mutation.mutate({ + id: team.id, + ...values, + theme: values.theme || null, + }); + }}> +
+
+

{t("theme")}

+

{t("theme_applies_note")}

- )} - +
+
+ + + +
+ +
+
+
+

{t("custom_brand_colors")}

+

{t("customize_your_brand_colors")}

+
+
+ +
+ ( +
+

{t("light_brand_color")}

+ form.setValue("brandColor", value, { shouldDirty: true })} + /> +
+ )} + /> + ( +
+

{t("dark_brand_color")}

+ form.setValue("darkBrandColor", value, { shouldDirty: true })} + /> +
+ )} + /> +
+
+ +
+
+
+ +

+ {t("team_disable_cal_branding_description", { appName: APP_NAME })} +

+
+ +
+ ( + { + form.setValue("hideBranding", isChecked); + }} + /> + )} + /> +
+
+
+
+ +

{t("hide_book_a_team_member_description")}

+
+
+ ( + { + form.setValue("hideBookATeamMember", isChecked); + }} + /> + )} + /> +
+
+
+ +
+ ) : ( +
+ {t("only_owner_change")} +
)} ); diff --git a/packages/features/settings/ThemeLabel.tsx b/packages/features/settings/ThemeLabel.tsx new file mode 100644 index 0000000000..6329845869 --- /dev/null +++ b/packages/features/settings/ThemeLabel.tsx @@ -0,0 +1,35 @@ +interface ThemeLabelProps { + variant: "light" | "dark" | "system"; + value?: "light" | "dark" | null; + label: string; + defaultChecked?: boolean; + register: any; +} + +export default function ThemeLabel(props: ThemeLabelProps) { + const { variant, label, value, defaultChecked, register } = props; + + return ( + + ); +} diff --git a/packages/lib/server/queries/teams/index.ts b/packages/lib/server/queries/teams/index.ts index 3774f1fa85..d0c708faac 100644 --- a/packages/lib/server/queries/teams/index.ts +++ b/packages/lib/server/queries/teams/index.ts @@ -33,6 +33,9 @@ export async function getTeamWithMembers(id?: number, slug?: string, userId?: nu }, }, }, + theme: true, + brandColor: true, + darkBrandColor: true, eventTypes: { where: { hidden: false, diff --git a/packages/prisma/migrations/20230131062229_add_theme_and_brand_colors_for_teams/migration.sql b/packages/prisma/migrations/20230131062229_add_theme_and_brand_colors_for_teams/migration.sql new file mode 100644 index 0000000000..13a7b753ef --- /dev/null +++ b/packages/prisma/migrations/20230131062229_add_theme_and_brand_colors_for_teams/migration.sql @@ -0,0 +1,4 @@ +-- AlterTable +ALTER TABLE "Team" ADD COLUMN "brandColor" TEXT NOT NULL DEFAULT '#292929', +ADD COLUMN "darkBrandColor" TEXT NOT NULL DEFAULT '#fafafa', +ADD COLUMN "theme" TEXT; diff --git a/packages/prisma/schema.prisma b/packages/prisma/schema.prisma index 60aa0ba714..cd6b135753 100644 --- a/packages/prisma/schema.prisma +++ b/packages/prisma/schema.prisma @@ -217,6 +217,9 @@ model Team { createdAt DateTime @default(now()) /// @zod.custom(imports.teamMetadataSchema) metadata Json? + theme String? + brandColor String @default("#292929") + darkBrandColor String @default("#fafafa") } enum MembershipRole { diff --git a/packages/trpc/server/routers/viewer/teams.tsx b/packages/trpc/server/routers/viewer/teams.tsx index 5787b42b10..e96543e124 100644 --- a/packages/trpc/server/routers/viewer/teams.tsx +++ b/packages/trpc/server/routers/viewer/teams.tsx @@ -125,6 +125,9 @@ export const viewerTeamsRouter = router({ slug: z.string().optional(), hideBranding: z.boolean().optional(), hideBookATeamMember: z.boolean().optional(), + brandColor: z.string().optional(), + darkBrandColor: z.string().optional(), + theme: z.string().optional().nullable(), }) ) .mutation(async ({ ctx, input }) => { @@ -153,6 +156,9 @@ export const viewerTeamsRouter = router({ bio: input.bio, hideBranding: input.hideBranding, hideBookATeamMember: input.hideBookATeamMember, + brandColor: input.brandColor, + darkBrandColor: input.darkBrandColor, + theme: input.theme, }; if (