diff --git a/apps/web/components/v2/settings/CalendarSwitch.tsx b/apps/web/components/v2/settings/CalendarSwitch.tsx
new file mode 100644
index 0000000000..9642ab7f17
--- /dev/null
+++ b/apps/web/components/v2/settings/CalendarSwitch.tsx
@@ -0,0 +1,84 @@
+import { useMutation } from "react-query";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Icon } from "@calcom/ui";
+import Badge from "@calcom/ui/v2/core/Badge";
+import Switch from "@calcom/ui/v2/core/Switch";
+import showToast from "@calcom/ui/v2/core/notfications";
+
+export function CalendarSwitch(props: {
+ type: string;
+ externalId: string;
+ title: string;
+ defaultSelected: boolean;
+}) {
+ const { t } = useLocale();
+
+ const utils = trpc.useContext();
+
+ const mutation = useMutation<
+ unknown,
+ unknown,
+ {
+ isOn: boolean;
+ }
+ >(
+ async ({ isOn }) => {
+ const body = {
+ integration: props.type,
+ externalId: props.externalId,
+ };
+ if (isOn) {
+ const res = await fetch("/api/availability/calendar", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+ if (!res.ok) {
+ throw new Error("Something went wrong");
+ }
+ } else {
+ const res = await fetch("/api/availability/calendar", {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!res.ok) {
+ throw new Error("Something went wrong");
+ }
+ }
+ },
+ {
+ async onSettled() {
+ await utils.invalidateQueries(["viewer.integrations"]);
+ },
+ onError() {
+ showToast(`Something went wrong when toggling "${props.title}""`, "error");
+ },
+ }
+ );
+ return (
+
+ {
+ mutation.mutate({ isOn });
+ }}
+ />
+ {props.defaultSelected && (
+
+ {t("adding_events_to")}
+
+ )}
+
+ );
+}
diff --git a/apps/web/components/v2/settings/ImageUploader.tsx b/apps/web/components/v2/settings/ImageUploader.tsx
new file mode 100644
index 0000000000..e31a6dd87a
--- /dev/null
+++ b/apps/web/components/v2/settings/ImageUploader.tsx
@@ -0,0 +1,169 @@
+import { FormEvent, useCallback, useEffect, useState } from "react";
+import Cropper from "react-easy-crop";
+
+import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
+import Button from "@calcom/ui/v2/core/Button";
+
+import { Area, getCroppedImg } from "@lib/cropImage";
+import { useFileReader } from "@lib/hooks/useFileReader";
+import { useLocale } from "@lib/hooks/useLocale";
+
+import Slider from "@components/Slider";
+
+type ImageUploaderProps = {
+ id: string;
+ buttonMsg: string;
+ handleAvatarChange: (imageSrc: string) => void;
+ imageSrc?: string;
+ target: string;
+};
+
+interface FileEvent extends FormEvent {
+ target: EventTarget & T;
+}
+
+// This is separate to prevent loading the component until file upload
+function CropContainer({
+ onCropComplete,
+ imageSrc,
+}: {
+ imageSrc: string;
+ onCropComplete: (croppedAreaPixels: Area) => void;
+}) {
+ const { t } = useLocale();
+ const [crop, setCrop] = useState({ x: 0, y: 0 });
+ const [zoom, setZoom] = useState(1);
+
+ const handleZoomSliderChange = (value: number) => {
+ value < 1 ? setZoom(1) : setZoom(value);
+ };
+
+ return (
+
+
+ onCropComplete(croppedAreaPixels)}
+ onZoomChange={setZoom}
+ />
+
+
+
+ );
+}
+
+export default function ImageUploader({
+ target,
+ id,
+ buttonMsg,
+ handleAvatarChange,
+ ...props
+}: ImageUploaderProps) {
+ const { t } = useLocale();
+ const [imageSrc, setImageSrc] = useState(null);
+ const [croppedAreaPixels, setCroppedAreaPixels] = useState(null);
+
+ const [{ result }, setFile] = useFileReader({
+ method: "readAsDataURL",
+ });
+
+ useEffect(() => {
+ if (props.imageSrc) setImageSrc(props.imageSrc);
+ }, [props.imageSrc]);
+
+ const onInputFile = (e: FileEvent) => {
+ if (!e.target.files?.length) {
+ return;
+ }
+ setFile(e.target.files[0]);
+ };
+
+ const showCroppedImage = useCallback(
+ async (croppedAreaPixels: Area | null) => {
+ try {
+ if (!croppedAreaPixels) return;
+ const croppedImage = await getCroppedImg(
+ result as string /* result is always string when using readAsDataUrl */,
+ croppedAreaPixels
+ );
+ setImageSrc(croppedImage);
+ handleAvatarChange(croppedImage);
+ } catch (e) {
+ console.error(e);
+ }
+ },
+ [result, handleAvatarChange]
+ );
+
+ return (
+
+ );
+}
diff --git a/apps/web/middleware.ts b/apps/web/middleware.ts
index 900cdae271..09ed4c0183 100644
--- a/apps/web/middleware.ts
+++ b/apps/web/middleware.ts
@@ -5,7 +5,13 @@ import { CONSOLE_URL, WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
import { isIpInBanlist } from "@calcom/lib/getIP";
import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry";
-const V2_WHITELIST = ["/settings/admin", "/availability", "/bookings", "/event-types"];
+const V2_WHITELIST = [
+ "/settings/admin",
+ "/settings/my-account",
+ "/availability",
+ "/bookings",
+ "/event-types",
+];
const middleware: NextMiddleware = async (req) => {
const url = req.nextUrl;
diff --git a/apps/web/next.config.js b/apps/web/next.config.js
index bb1b38a732..789e2f3e83 100644
--- a/apps/web/next.config.js
+++ b/apps/web/next.config.js
@@ -126,6 +126,13 @@ const nextConfig = {
destination: "/settings/profile",
permanent: true,
},
+ /* V2 testers get redirected to the new settings */
+ {
+ source: "/settings/profile",
+ has: [{ type: "cookie", key: "calcom-v2-early-access" }],
+ destination: "/settings/my-account/profile",
+ permanent: false,
+ },
{
source: "/bookings",
destination: "/bookings/upcoming",
diff --git a/apps/web/pages/v2/settings/my-account/appearance.tsx b/apps/web/pages/v2/settings/my-account/appearance.tsx
new file mode 100644
index 0000000000..8d8565bacb
--- /dev/null
+++ b/apps/web/pages/v2/settings/my-account/appearance.tsx
@@ -0,0 +1,212 @@
+import { GetServerSidePropsContext } from "next";
+import { Trans } from "next-i18next";
+import { useRouter } from "next/router";
+import { title } from "process";
+import { useMemo, useState } from "react";
+import { useForm, Controller } from "react-hook-form";
+
+import { WEBAPP_URL } from "@calcom/lib/constants";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import prisma from "@calcom/prisma";
+import { trpc } from "@calcom/trpc/react";
+import { Icon } from "@calcom/ui";
+import Avatar from "@calcom/ui/v2/core/Avatar";
+import Badge from "@calcom/ui/v2/core/Badge";
+import { Button } from "@calcom/ui/v2/core/Button";
+import Loader from "@calcom/ui/v2/core/Loader";
+import Switch from "@calcom/ui/v2/core/Switch";
+import TimezoneSelect from "@calcom/ui/v2/core/TimezoneSelect";
+import ColorPicker from "@calcom/ui/v2/core/colorpicker";
+import Select from "@calcom/ui/v2/core/form/Select";
+import { TextField, Form, Label } from "@calcom/ui/v2/core/form/fields";
+import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
+import showToast from "@calcom/ui/v2/core/notfications";
+
+import { getSession } from "@lib/auth";
+import { inferSSRProps } from "@lib/types/inferSSRProps";
+
+const AppearanceView = (props: inferSSRProps) => {
+ const { t } = useLocale();
+ const { user } = props;
+
+ const mutation = trpc.useMutation("viewer.updateProfile", {
+ onSuccess: () => {
+ showToast(t("settings_updated_successfully"), "success");
+ },
+ onError: () => {
+ showToast(t("error_updating_settings"), "error");
+ },
+ });
+
+ const themeOptions = [
+ { value: "light", label: t("light") },
+ { value: "dark", label: t("dark") },
+ ];
+
+ const formMethods = useForm();
+
+ return (
+
+ );
+};
+
+AppearanceView.getLayout = getLayout;
+
+export default AppearanceView;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const session = await getSession(context);
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const user = await prisma.user.findUnique({
+ where: {
+ id: session.user.id,
+ },
+ select: {
+ username: true,
+ timeZone: true,
+ timeFormat: true,
+ weekStart: true,
+ brandColor: true,
+ darkBrandColor: true,
+ hideBranding: true,
+ theme: true,
+ eventTypes: {
+ select: {
+ title: true,
+ },
+ },
+ },
+ });
+
+ if (!user) {
+ throw new Error("User seems logged in but cannot be found in the db");
+ }
+
+ return {
+ props: {
+ user,
+ },
+ };
+};
diff --git a/apps/web/pages/v2/settings/my-account/calendars.tsx b/apps/web/pages/v2/settings/my-account/calendars.tsx
new file mode 100644
index 0000000000..42058f3e17
--- /dev/null
+++ b/apps/web/pages/v2/settings/my-account/calendars.tsx
@@ -0,0 +1,126 @@
+import { Trans } from "next-i18next";
+import Link from "next/link";
+import { Fragment } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Icon } from "@calcom/ui";
+import Badge from "@calcom/ui/v2/core/Badge";
+import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
+import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
+import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
+import DestinationCalendarSelector from "@calcom/ui/v2/modules/event-types/DestinationCalendarSelector";
+import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
+
+import { QueryCell } from "@lib/QueryCell";
+
+import { CalendarSwitch } from "@components/v2/settings/CalendarSwitch";
+
+const CalendarsView = () => {
+ const { t } = useLocale();
+
+ const utils = trpc.useContext();
+
+ const query = trpc.useQuery(["viewer.connectedCalendars"]);
+ const mutation = trpc.useMutation("viewer.setDestinationCalendar", {
+ async onSettled() {
+ await utils.invalidateQueries(["viewer.connectedCalendars"]);
+ },
+ });
+
+ return (
+ {
+ console.log("🚀 ~ file: calendars.tsx ~ line 28 ~ CalendarsView ~ data", data);
+ return data.connectedCalendars.length ? (
+
+
+
+
+
+
{t("add_to_calendar")}
+
+
+ Where to add events when you re booked. You can override this on a per-event basis in
+ advanced settings in the event type.
+
+
+
+
+
+
{t("check_for_conflicts")}
+
{t("select_calendars")}
+
+ {data.connectedCalendars.map((item) => (
+
+ {item.calendars && (
+
+
+ {
+ // eslint-disable-next-line @next/next/no-img-element
+ item.integration.logo && (
+

+ )
+ }
+
+
+
+ {item.integration.name || item.integration.title}
+
+ {data?.destinationCalendar?.credentialId === item.credentialId && (
+ Default
+ )}
+
+ {item.integration.description}
+
+
+
+
+
+
+
{t("toggle_calendars_conflict")}
+
+ {item.calendars.map((cal) => (
+
+ ))}
+
+
+
+ )}
+
+ ))}
+
+
+ ) : (
+ console.log("Button Clicked")}
+ />
+ );
+ }}
+ />
+ );
+};
+
+CalendarsView.getLayout = getLayout;
+
+export default CalendarsView;
diff --git a/apps/web/pages/v2/settings/my-account/conferencing.tsx b/apps/web/pages/v2/settings/my-account/conferencing.tsx
new file mode 100644
index 0000000000..e23b2d61e7
--- /dev/null
+++ b/apps/web/pages/v2/settings/my-account/conferencing.tsx
@@ -0,0 +1,100 @@
+import { GetServerSidePropsContext } from "next";
+
+import getApps from "@calcom/app-store/utils";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import prisma from "@calcom/prisma";
+import { Icon } from "@calcom/ui";
+import Dropdown, {
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@calcom/ui/v2/core/Dropdown";
+import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
+import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
+
+import { getSession } from "@lib/auth";
+import { inferSSRProps } from "@lib/types/inferSSRProps";
+
+const ConferencingLayout = (props: inferSSRProps) => {
+ const { t } = useLocale();
+
+ const { apps } = props;
+
+ // Error reason: getaddrinfo EAI_AGAIN http
+ // const query = trpc.useQuery(["viewer.integrations", { variant: "conferencing", onlyInstalled: true }], {
+ // suspense: true,
+ // });
+
+ return (
+
+ {apps.map((app) => (
+
+

+
+
+
{app.title}
+
{app.description}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ))}
+
+ );
+};
+
+ConferencingLayout.getLayout = getLayout;
+
+export default ConferencingLayout;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const session = await getSession(context);
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const videoCredentials = await prisma.credential.findMany({
+ where: {
+ userId: session.user.id,
+ app: {
+ categories: {
+ has: "video",
+ },
+ },
+ },
+ });
+
+ const apps = getApps(videoCredentials)
+ .filter((app) => {
+ return app.variant === "conferencing" && app.credentials.length;
+ })
+ .map((app) => {
+ return {
+ slug: app.slug,
+ title: app.title,
+ logo: app.logo,
+ description: app.description,
+ credentialId: app.credentials[0].id,
+ isGlobal: app.isGlobal,
+ };
+ });
+
+ return { props: { apps } };
+};
diff --git a/apps/web/pages/v2/settings/my-account/general.tsx b/apps/web/pages/v2/settings/my-account/general.tsx
new file mode 100644
index 0000000000..2a2471e726
--- /dev/null
+++ b/apps/web/pages/v2/settings/my-account/general.tsx
@@ -0,0 +1,214 @@
+import { GetServerSidePropsContext } from "next";
+import { TFunction } from "next-i18next";
+import { useRouter } from "next/router";
+import { useMemo } from "react";
+import { useForm, Controller } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import prisma from "@calcom/prisma";
+import { trpc } from "@calcom/trpc/react";
+import { Button } from "@calcom/ui/v2/core/Button";
+import TimezoneSelect from "@calcom/ui/v2/core/TimezoneSelect";
+import Select from "@calcom/ui/v2/core/form/Select";
+import { Form, Label } from "@calcom/ui/v2/core/form/fields";
+import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
+import showToast from "@calcom/ui/v2/core/notfications";
+
+import { withQuery } from "@lib/QueryCell";
+import { getSession } from "@lib/auth";
+import { nameOfDay } from "@lib/core/i18n/weekday";
+import { inferSSRProps } from "@lib/types/inferSSRProps";
+
+interface GeneralViewProps {
+ localeProp: string;
+ t: TFunction;
+ user: {
+ timeZone: string;
+ timeFormat: number | null;
+ weekStart: string;
+ };
+}
+
+const WithQuery = withQuery(["viewer.public.i18n"], { context: { skipBatch: true } });
+
+const GeneralQueryView = (props: inferSSRProps) => {
+ const { t } = useLocale();
+
+ return } />;
+};
+
+const GeneralView = ({ localeProp, t, user }: GeneralViewProps) => {
+ const router = useRouter();
+
+ // const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
+ const mutation = trpc.useMutation("viewer.updateProfile", {
+ onSuccess: () => {
+ showToast(t("settings_updated_successfully"), "success");
+ },
+ onError: () => {
+ showToast(t("error_updating_settings"), "error");
+ },
+ });
+
+ const localeOptions = useMemo(() => {
+ return (router.locales || []).map((locale) => ({
+ value: locale,
+ label: new Intl.DisplayNames(localeProp, { type: "language" }).of(locale) || "",
+ }));
+ }, [localeProp, router.locales]);
+
+ const timeFormatOptions = [
+ { value: 12, label: t("12_hour") },
+ { value: 24, label: t("24_hour") },
+ ];
+
+ const weekStartOptions = [
+ { value: "Sunday", label: nameOfDay(localeProp, 0) },
+ { value: "Monday", label: nameOfDay(localeProp, 1) },
+ { value: "Tuesday", label: nameOfDay(localeProp, 2) },
+ { value: "Wednesday", label: nameOfDay(localeProp, 3) },
+ { value: "Thursday", label: nameOfDay(localeProp, 4) },
+ { value: "Friday", label: nameOfDay(localeProp, 5) },
+ { value: "Saturday", label: nameOfDay(localeProp, 6) },
+ ];
+
+ const formMethods = useForm({
+ defaultValues: {
+ locale: {
+ value: localeProp || "",
+ label: localeOptions.find((option) => option.value === localeProp)?.label || "",
+ },
+ timeZone: user?.timeZone || "",
+ timeFormat: {
+ value: user?.timeFormat || 12,
+ label: timeFormatOptions.find((option) => option.value === user?.timeFormat)?.label || 12,
+ },
+ weekStart: {
+ value: user?.weekStart,
+ label: nameOfDay(localeProp, user?.weekStart === "Sunday" ? 0 : 1),
+ },
+ },
+ });
+
+ return (
+
+ );
+};
+
+GeneralQueryView.getLayout = getLayout;
+
+export default GeneralQueryView;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const session = await getSession(context);
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const user = await prisma.user.findUnique({
+ where: {
+ id: session.user.id,
+ },
+ select: {
+ timeZone: true,
+ timeFormat: true,
+ weekStart: true,
+ },
+ });
+
+ if (!user) {
+ throw new Error("User seems logged in but cannot be found in the db");
+ }
+
+ return {
+ props: {
+ user,
+ },
+ };
+};
diff --git a/apps/web/pages/v2/settings/my-account/profile.tsx b/apps/web/pages/v2/settings/my-account/profile.tsx
new file mode 100644
index 0000000000..d33da3a381
--- /dev/null
+++ b/apps/web/pages/v2/settings/my-account/profile.tsx
@@ -0,0 +1,219 @@
+import crypto from "crypto";
+import { GetServerSidePropsContext } from "next";
+import { signOut } from "next-auth/react";
+import { Trans } from "next-i18next";
+import { useState, useRef } from "react";
+import { useForm, Controller } from "react-hook-form";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import prisma from "@calcom/prisma";
+import { trpc } from "@calcom/trpc/react";
+import { Icon } from "@calcom/ui";
+import Avatar from "@calcom/ui/v2/core/Avatar";
+import { Button } from "@calcom/ui/v2/core/Button";
+import { Dialog, DialogTrigger, DialogContent } from "@calcom/ui/v2/core/Dialog";
+import { TextField, Form, Label } from "@calcom/ui/v2/core/form/fields";
+import { getLayout } from "@calcom/ui/v2/core/layouts/AdminLayout";
+import showToast from "@calcom/ui/v2/core/notfications";
+
+import { getSession } from "@lib/auth";
+import { inferSSRProps } from "@lib/types/inferSSRProps";
+
+import ImageUploader from "@components/v2/settings/ImageUploader";
+
+const ProfileView = (props: inferSSRProps) => {
+ const { t } = useLocale();
+
+ const { user } = props;
+ // const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
+ const mutation = trpc.useMutation("viewer.updateProfile", {
+ onSuccess: () => {
+ showToast(t("settings_updated_successfully"), "success");
+ },
+ onError: () => {
+ showToast(t("error_updating_settings"), "error");
+ },
+ });
+
+ const [deleteAccountOpen, setDeleteAccountOpen] = useState(false);
+
+ const deleteAccount = async () => {
+ await fetch("/api/user/me", {
+ method: "DELETE",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ }).catch((e) => {
+ console.error(`Error Removing user: ${user?.id}, email: ${user?.email} :`, e);
+ });
+ if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
+ signOut({ callbackUrl: "/auth/logout?survey=true" });
+ } else {
+ signOut({ callbackUrl: "/auth/logout" });
+ }
+ };
+
+ const formMethods = useForm({
+ defaultValues: {
+ avatar: user.avatar || "",
+ username: user?.username || "",
+ name: user?.name || "",
+ bio: user?.bio || "",
+ },
+ });
+
+ const avatarRef = useRef(null!);
+
+ return (
+ <>
+
+ >
+ );
+};
+
+ProfileView.getLayout = getLayout;
+
+export default ProfileView;
+
+export const getServerSideProps = async (context: GetServerSidePropsContext) => {
+ const session = await getSession(context);
+
+ if (!session?.user?.id) {
+ return { redirect: { permanent: false, destination: "/auth/login" } };
+ }
+
+ const user = await prisma.user.findUnique({
+ where: {
+ id: session.user.id,
+ },
+ select: {
+ id: true,
+ username: true,
+ email: true,
+ name: true,
+ bio: true,
+ avatar: true,
+ },
+ });
+
+ if (!user) {
+ throw new Error("User seems logged in but cannot be found in the db");
+ }
+
+ return {
+ props: {
+ user: {
+ ...user,
+ emailMd5: crypto.createHash("md5").update(user.email).digest("hex"),
+ },
+ },
+ };
+};
diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 777c0e385d..8de61fc84a 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -1055,5 +1055,21 @@
"event_advanced_tab_title":"Advanced",
"select_which_cal":"Select which calendar to add bookings to",
"custom_event_name":"Custom event name",
- "custom_event_name_description":"Create customised event names to display on calendar event"
+ "custom_event_name_description":"Create customised event names to display on calendar event",
+ "settings_updated_successfully": "Settings updated successfully",
+ "error_updating_settings":"Error updating settings",
+ "personal_cal_url": "My personal Cal URL",
+ "bio_hint": "A few sentences about yourself. this will appear on your personal url page.",
+ "delete_account_modal_title": "Delete Account",
+ "confirm_delete_account_modal": "Are you sure you want to delete your Cal.com account?",
+ "delete_my_account": "Delete my account",
+ "start_of_week": "Start of week",
+ "select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
+ "check_for_conflicts": "Check for conflicts",
+ "adding_events_to": "Adding events to",
+ "follow_system_preferences": "Follow system preferences",
+ "custom_brand_colors": "Custom brand colors",
+ "customize_your_brand_colors": "Customize your own brand colour into your booking page.",
+ "pro": "Pro",
+ "removes_cal_branding": "Removes any Cal related brandings, i.e. 'Powered by Cal.'"
}
diff --git a/packages/ui/v2/core/Shell.tsx b/packages/ui/v2/core/Shell.tsx
index f17fc081f1..3f7b076263 100644
--- a/packages/ui/v2/core/Shell.tsx
+++ b/packages/ui/v2/core/Shell.tsx
@@ -135,7 +135,7 @@ const Layout = (props: LayoutProps) => {
-
+ {props.SidebarContainer ||
}
@@ -153,6 +153,7 @@ type LayoutProps = {
children: ReactNode;
CTA?: ReactNode;
large?: boolean;
+ SidebarContainer?: ReactNode;
HeadingLeftIcon?: ReactNode;
backPath?: string; // renders back button to specified path
// use when content needs to expand with flex
diff --git a/packages/ui/v2/core/layouts/SettingsLayout.tsx b/packages/ui/v2/core/layouts/SettingsLayout.tsx
index ecf230379c..25b8e6b693 100644
--- a/packages/ui/v2/core/layouts/SettingsLayout.tsx
+++ b/packages/ui/v2/core/layouts/SettingsLayout.tsx
@@ -2,6 +2,7 @@ import React, { ComponentProps } from "react";
import { Icon } from "../../../Icon";
import Shell from "../Shell";
+import { VerticalTabItem } from "../navigation/tabs";
import VerticalTabs from "../navigation/tabs/VerticalTabs";
const tabs = [
@@ -10,12 +11,13 @@ const tabs = [
href: "/settings/profile",
icon: Icon.FiUser,
children: [
- { name: "profile", href: "/settings/profile" },
- { name: "general", href: "/settings/profile" },
- { name: "calendars", href: "/settings/profile" },
- { name: "conferencing", href: "/settings/profile" },
- { name: "appearance", href: "/settings/profile" },
- { name: "referrals", href: "/settings/profile" },
+ { name: "profile", href: "/settings/my-account/profile" },
+ { name: "general", href: "/settings/my-account/general" },
+ { name: "calendars", href: "/settings/my-account/calendars" },
+ { name: "conferencing", href: "/settings/my-account/conferencing" },
+ { name: "appearance", href: "/settings/my-account/appearance" },
+ // TODO
+ { name: "referrals", href: "/settings/my-account/referrals" },
],
},
{
@@ -72,10 +74,20 @@ export default function SettingsLayout({
...rest
}: { children: React.ReactNode } & ComponentProps
) {
return (
-
-
-
-
+
+
+
+ }>
{children}
);
diff --git a/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx b/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx
index 27850646ef..ce8e244d2a 100644
--- a/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx
+++ b/packages/ui/v2/core/navigation/tabs/VerticalTabItem.tsx
@@ -14,6 +14,8 @@ export type VerticalTabItemProps = {
icon?: SVGComponent;
disabled?: boolean;
children?: VerticalTabItemProps[];
+ textClassNames?: string;
+ className?: string;
isChild?: boolean;
} & (
| {
@@ -59,19 +61,17 @@ const VerticalTabItem: FC = ({ name, href, tabName, info,
{props.icon && }
-
-
{t(name)}
+
+
{t(name)}
{info &&
{t(info)}
}
{/* {isCurrent && (
diff --git a/packages/ui/v2/core/navigation/tabs/VerticalTabs.tsx b/packages/ui/v2/core/navigation/tabs/VerticalTabs.tsx
index b770c2f187..3d85409baa 100644
--- a/packages/ui/v2/core/navigation/tabs/VerticalTabs.tsx
+++ b/packages/ui/v2/core/navigation/tabs/VerticalTabs.tsx
@@ -4,17 +4,18 @@ import VerticalTabItem, { VerticalTabItemProps } from "./VerticalTabItem";
export interface NavTabProps {
tabs: VerticalTabItemProps[];
+ children?: React.ReactNode;
+ className?: string;
}
-const NavTabs: FC
= ({ tabs, ...props }) => {
+const NavTabs: FC = ({ tabs, className = "", ...props }) => {
return (
- <>
-
- >
+
);
};
diff --git a/packages/ui/v2/modules/List.tsx b/packages/ui/v2/modules/List.tsx
new file mode 100644
index 0000000000..94cc0523b5
--- /dev/null
+++ b/packages/ui/v2/modules/List.tsx
@@ -0,0 +1,72 @@
+import Link from "next/link";
+import { createElement } from "react";
+
+import classNames from "@calcom/lib/classNames";
+
+export function List(props: JSX.IntrinsicElements["ul"]) {
+ return (
+
+ );
+}
+
+export type ListItemProps = { expanded?: boolean } & ({ href?: never } & JSX.IntrinsicElements["li"]);
+
+export function ListItem(props: ListItemProps) {
+ const { href, expanded, ...passThroughProps } = props;
+
+ const elementType = href ? "a" : "li";
+
+ const element = createElement(
+ elementType,
+ {
+ ...passThroughProps,
+ className: classNames(
+ "items-center rounded-md bg-white min-w-0 flex-1 flex border-neutral-200 p-2 sm:mx-0 sm:p-8 md:border md:p-4 xl:mt-0",
+ expanded ? "my-2 border" : "border -mb-px last:mb-0",
+ props.className,
+ (props.onClick || href) && "hover:bg-neutral-50"
+ ),
+ },
+ props.children
+ );
+
+ return href ? (
+
+ {element}
+
+ ) : (
+ element
+ );
+}
+
+export function ListItemTitle(
+ props: JSX.IntrinsicElements[TComponent] & { component?: TComponent }
+) {
+ const { component = "span", ...passThroughProps } = props;
+
+ return createElement(
+ component,
+ {
+ ...passThroughProps,
+ className: classNames("text-sm font-medium text-neutral-900 truncate", props.className),
+ },
+ props.children
+ );
+}
+
+export function ListItemText(
+ props: JSX.IntrinsicElements[TComponent] & { component?: TComponent }
+) {
+ const { component = "span", ...passThroughProps } = props;
+
+ return createElement(
+ component,
+ {
+ ...passThroughProps,
+ className: classNames("text-sm text-gray-500 truncate", props.className),
+ },
+ props.children
+ );
+}
diff --git a/packages/ui/v2/modules/integrations/DisconnectIntegration.tsx b/packages/ui/v2/modules/integrations/DisconnectIntegration.tsx
new file mode 100644
index 0000000000..41b284f337
--- /dev/null
+++ b/packages/ui/v2/modules/integrations/DisconnectIntegration.tsx
@@ -0,0 +1,54 @@
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { trpc } from "@calcom/trpc/react";
+import { Icon } from "@calcom/ui";
+import { Button } from "@calcom/ui/v2/core/Button";
+import { Dialog, DialogTrigger, DialogContent } from "@calcom/ui/v2/core/Dialog";
+import showToast from "@calcom/ui/v2/core/notfications";
+
+export default function DisconnectIntegration({
+ credentialId,
+ label,
+ trashIcon,
+ isGlobal,
+}: {
+ credentialId: number;
+ label: string;
+ trashIcon?: boolean;
+ isGlobal?: boolean;
+}) {
+ const { t } = useLocale();
+ const [modalOpen, setModalOpen] = useState(false);
+
+ const mutation = trpc.useMutation("viewer.deleteCredential", {
+ onSuccess: () => {
+ showToast("Integration deleted successfully", "success");
+ setModalOpen(false);
+ },
+ onError: () => {
+ showToast("Error deleting app", "error");
+ setModalOpen(false);
+ },
+ });
+
+ return (
+ <>
+
+ >
+ );
+}