diff --git a/apps/web/public/static/locales/en/common.json b/apps/web/public/static/locales/en/common.json
index 6426a56d10..e8bd920484 100644
--- a/apps/web/public/static/locales/en/common.json
+++ b/apps/web/public/static/locales/en/common.json
@@ -4063,6 +4063,9 @@
"no_available_users_found_input": "No available users found",
"repeats_num_times": "Repeats {{count}} times",
"view_booking_details": "View Booking Details",
+ "cannot_delete_event_type": "This event type cannot be deleted",
+ "delete_event_type_confirmation": "Are you sure you want to delete this event type?",
+ "error_loading_event_types": "Error loading event types",
"recurring_booking_description": "Recurring {{frequency}} event with {{count}} occurrences",
"team_bio": "Team Bio",
"team_bio_placeholder": "Tell us about your team...",
diff --git a/packages/app-store/_utils/getBulkEventTypes.test.ts b/packages/app-store/_utils/getBulkEventTypes.test.ts
index c5af114a89..77b0b96790 100644
--- a/packages/app-store/_utils/getBulkEventTypes.test.ts
+++ b/packages/app-store/_utils/getBulkEventTypes.test.ts
@@ -265,6 +265,9 @@ describe("getBulkUserEventTypes", () => {
select: {
id: true,
title: true,
+ description: true,
+ length: true,
+ slug: true,
locations: true,
metadata: true,
parentId: true,
diff --git a/packages/app-store/_utils/getBulkEventTypes.ts b/packages/app-store/_utils/getBulkEventTypes.ts
index 9876d33466..d38b893e2f 100644
--- a/packages/app-store/_utils/getBulkEventTypes.ts
+++ b/packages/app-store/_utils/getBulkEventTypes.ts
@@ -40,6 +40,9 @@ export const getBulkUserEventTypes = async (userId: number) => {
select: {
id: true,
title: true,
+ slug: true,
+ description: true,
+ length: true,
locations: true,
metadata: true,
parentId: true,
@@ -65,6 +68,9 @@ export const getBulkTeamEventTypes = async (teamId: number) => {
select: {
id: true,
title: true,
+ slug: true,
+ description: true,
+ length: true,
locations: true,
},
});
diff --git a/packages/platform/atoms/event-types/__tests__/EventTypeListItem.test.tsx b/packages/platform/atoms/event-types/__tests__/EventTypeListItem.test.tsx
new file mode 100644
index 0000000000..f70f780a6f
--- /dev/null
+++ b/packages/platform/atoms/event-types/__tests__/EventTypeListItem.test.tsx
@@ -0,0 +1,194 @@
+import { render, screen } from "@testing-library/react";
+import { vi } from "vitest";
+
+import type { AtomEventTypeListItem } from "../types";
+import { EventTypeListItem } from "../components/EventTypeListItem";
+
+// Mock auto-animate
+vi.mock("@formkit/auto-animate/react", () => ({
+ useAutoAnimate: () => [null],
+}));
+
+// Mock Next.js Link
+vi.mock("next/link", () => ({
+ __esModule: true,
+ default: ({ children, href }: { children: React.ReactNode; href: string }) => {children},
+}));
+
+// Mock useLocale
+vi.mock("@calcom/lib/hooks/useLocale", () => ({
+ useLocale: () => ({
+ t: (key: string) => key,
+ }),
+}));
+
+// Mock toast
+vi.mock("@calcom/ui/components/toast", () => ({
+ showToast: vi.fn(),
+}));
+
+// Mock dialog components
+vi.mock("@calcom/ui/components/dialog", () => ({
+ Dialog: ({ children, open }: { children: React.ReactNode; open: boolean }) =>
+ open ?
{children}
: null,
+ ConfirmationDialogContent: ({
+ children,
+ onConfirm,
+ }: {
+ children: React.ReactNode;
+ onConfirm: () => void;
+ }) => (
+
+ {children}
+
+
+ ),
+}));
+
+describe("EventTypeListItem", () => {
+ const mockEventType: AtomEventTypeListItem = {
+ id: 1,
+ title: "30 Min Meeting",
+ slug: "30min",
+ description: "Quick meeting",
+ length: 30,
+ locations: null,
+ logo: "/test-logo.svg",
+ };
+
+ const mockDeleteFunction = vi.fn();
+
+ const renderComponent = ({
+ eventType = mockEventType,
+ deleteFunction = mockDeleteFunction,
+ isDeletable = true,
+ getEventTypeUrl,
+ }: {
+ eventType?: typeof mockEventType;
+ deleteFunction?: typeof mockDeleteFunction;
+ isDeletable?: boolean;
+ getEventTypeUrl?: (id: number) => string;
+ } = {}) => {
+ return render(
+
+ );
+ };
+
+ it("should render event type details correctly", () => {
+ renderComponent();
+
+ expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
+ expect(screen.getByText("Quick meeting")).toBeInTheDocument();
+ expect(screen.getByText(/30m/)).toBeInTheDocument();
+ });
+
+ it("should render as link when getEventTypeUrl is provided", () => {
+ const getEventTypeUrl = (id: number) => `/event-types/${id}`;
+ renderComponent({ getEventTypeUrl });
+
+ const link = screen.getByRole("link", { name: /30 Min Meeting/i });
+ expect(link).toHaveAttribute("href", "/event-types/1");
+ });
+
+ it("should render without link when getEventTypeUrl is not provided", () => {
+ renderComponent();
+
+ const links = screen.queryAllByRole("link");
+ expect(links.length).toBe(0);
+ });
+
+ it("should display event type without description when description is null", () => {
+ renderComponent({
+ eventType: { ...mockEventType, description: null },
+ });
+
+ expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
+ expect(screen.queryByText("Quick meeting")).not.toBeInTheDocument();
+ expect(screen.getByText(/30m/)).toBeInTheDocument();
+ });
+
+ it("should render dropdown menu trigger", () => {
+ renderComponent();
+
+ const dropdownTrigger = screen.getByTestId("event-type-options-1");
+ expect(dropdownTrigger).toBeInTheDocument();
+ });
+
+ it("should render with unique data-testid based on event type ID", () => {
+ const customEventType = {
+ ...mockEventType,
+ id: 999,
+ };
+
+ renderComponent({ eventType: customEventType });
+
+ const dropdownTrigger = screen.getByTestId("event-type-options-999");
+ expect(dropdownTrigger).toBeInTheDocument();
+ });
+
+ it("should display different event durations correctly", () => {
+ const shortEvent = { ...mockEventType, length: 15 };
+ const { rerender } = renderComponent({ eventType: shortEvent });
+
+ expect(screen.getByText(/15m/)).toBeInTheDocument();
+
+ const longEvent = { ...mockEventType, length: 120 };
+ rerender(
+
+ );
+
+ expect(screen.getByText(/2h/)).toBeInTheDocument();
+ });
+
+ it("should render event type with long title without breaking layout", () => {
+ const longTitleEvent = {
+ ...mockEventType,
+ title: "This is a very long event type title that should be truncated properly in the UI",
+ };
+
+ renderComponent({ eventType: longTitleEvent });
+
+ expect(
+ screen.getByText("This is a very long event type title that should be truncated properly in the UI")
+ ).toBeInTheDocument();
+ expect(screen.getByText("Quick meeting")).toBeInTheDocument();
+ });
+
+ it("should render with locations as null", () => {
+ const eventType: AtomEventTypeListItem = {
+ ...mockEventType,
+ locations: null,
+ };
+
+ renderComponent({ eventType });
+ expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
+ });
+
+ it("should render without logo", () => {
+ const eventType: AtomEventTypeListItem = {
+ ...mockEventType,
+ logo: undefined,
+ };
+
+ renderComponent({ eventType });
+ expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
+ });
+
+ it("should render with locations object", () => {
+ const eventType: AtomEventTypeListItem = {
+ ...mockEventType,
+ locations: [{ type: "integrations:zoom" }],
+ logo: "/app-store/zoomvideo/icon.svg",
+ };
+
+ renderComponent({ eventType });
+ expect(screen.getByText("30 Min Meeting")).toBeInTheDocument();
+ });
+});
diff --git a/packages/platform/atoms/event-types/__tests__/formatEventTypeDuration.test.ts b/packages/platform/atoms/event-types/__tests__/formatEventTypeDuration.test.ts
new file mode 100644
index 0000000000..5e3128ebb3
--- /dev/null
+++ b/packages/platform/atoms/event-types/__tests__/formatEventTypeDuration.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+
+import { formatEventTypeDuration } from "../lib/formatEventTypeDuration";
+
+describe("formatEventTypeDuration", () => {
+ it("should format single digit minutes", () => {
+ expect(formatEventTypeDuration(5)).toBe("5m");
+ });
+
+ it("should format double digit minutes", () => {
+ expect(formatEventTypeDuration(15)).toBe("15m");
+ expect(formatEventTypeDuration(30)).toBe("30m");
+ expect(formatEventTypeDuration(45)).toBe("45m");
+ });
+
+ it("should format edge case at 59 minutes", () => {
+ expect(formatEventTypeDuration(59)).toBe("59m");
+ });
+
+ it("should handle edge case at 61 minutes", () => {
+ expect(formatEventTypeDuration(61)).toBe("1h 1m");
+ });
+
+ it("should format single hour", () => {
+ expect(formatEventTypeDuration(60)).toBe("1h");
+ });
+
+ it("should format multiple hours", () => {
+ expect(formatEventTypeDuration(120)).toBe("2h");
+ expect(formatEventTypeDuration(180)).toBe("3h");
+ expect(formatEventTypeDuration(240)).toBe("4h");
+ });
+
+ it("should format hours with minutes", () => {
+ expect(formatEventTypeDuration(75)).toBe("1h 15m");
+ expect(formatEventTypeDuration(150)).toBe("2h 30m");
+ expect(formatEventTypeDuration(195)).toBe("3h 15m");
+ });
+
+ it("should handle 1 minute", () => {
+ expect(formatEventTypeDuration(1)).toBe("1m");
+ });
+
+ it("should handle zero minutes", () => {
+ expect(formatEventTypeDuration(0)).toBe("0m");
+ });
+});
diff --git a/packages/platform/atoms/event-types/components/EventTypeListItem.tsx b/packages/platform/atoms/event-types/components/EventTypeListItem.tsx
new file mode 100644
index 0000000000..787efba10d
--- /dev/null
+++ b/packages/platform/atoms/event-types/components/EventTypeListItem.tsx
@@ -0,0 +1,151 @@
+"use client";
+
+import Link from "next/link";
+import { useState } from "react";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { Badge } from "@calcom/ui/components/badge";
+import { Button } from "@calcom/ui/components/button";
+import { Dialog, ConfirmationDialogContent } from "@calcom/ui/components/dialog";
+import {
+ Dropdown,
+ DropdownItem,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuSeparator,
+ DropdownMenuTrigger,
+} from "@calcom/ui/components/dropdown";
+import { showToast } from "@calcom/ui/components/toast";
+
+import type { AtomEventTypeListItem } from "../types";
+import { formatEventTypeDuration } from "../lib/formatEventTypeDuration";
+
+const EventTypeContent = ({ eventType }: { eventType: AtomEventTypeListItem }) => {
+ return (
+
+
+ {eventType.title}
+
+
+ {eventType.description && {eventType.description}}
+
+ {formatEventTypeDuration(eventType.length)}
+
+
+
+ );
+};
+
+type EventTypeActionsProps = {
+ eventType: AtomEventTypeListItem;
+ isDeletable: boolean;
+ getEventTypeUrl?: (eventTypeId: number) => string;
+ deleteFunction: ({ eventTypeId }: { eventTypeId: number }) => void;
+};
+
+const EventTypeActions = ({
+ eventType,
+ isDeletable,
+ getEventTypeUrl,
+ deleteFunction,
+}: EventTypeActionsProps) => {
+ const { t } = useLocale();
+ const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false);
+
+ return (
+ <>
+
+
+
+
+
+ {getEventTypeUrl && (
+
+
+ {t("edit")}
+
+
+ )}
+
+
+ {
+ if (!isDeletable) {
+ showToast(t("cannot_delete_event_type"), "error");
+ return;
+ }
+ setIsDeleteDialogOpen(true);
+ }}>
+ {t("delete")}
+
+
+
+
+
+
+ >
+ );
+};
+
+type EventTypeListItemProps = {
+ eventType: AtomEventTypeListItem;
+ deleteFunction: ({ eventTypeId }: { eventTypeId: number }) => void;
+ isDeletable?: boolean;
+ getEventTypeUrl?: (eventTypeId: number) => string;
+};
+
+export function EventTypeListItem({
+ eventType,
+ deleteFunction,
+ isDeletable = true,
+ getEventTypeUrl,
+}: EventTypeListItemProps) {
+ const Wrapper: React.ElementType = getEventTypeUrl ? Link : "div";
+ const wrapperProps = getEventTypeUrl
+ ? {
+ href: getEventTypeUrl(eventType.id),
+ title: eventType.title,
+ }
+ : {};
+
+ return (
+
+
+
+ );
+}
diff --git a/packages/platform/atoms/event-types/hooks/useAtomGetAllEventTypes.ts b/packages/platform/atoms/event-types/hooks/useAtomGetAllEventTypes.ts
new file mode 100644
index 0000000000..183870718f
--- /dev/null
+++ b/packages/platform/atoms/event-types/hooks/useAtomGetAllEventTypes.ts
@@ -0,0 +1,30 @@
+import { useQuery } from "@tanstack/react-query";
+
+import { V2_ENDPOINTS, SUCCESS_STATUS } from "@calcom/platform-constants";
+import type { ApiResponse } from "@calcom/platform-types";
+
+import { useAtomsContext } from "../../hooks/useAtomsContext";
+import http from "../../lib/http";
+import type { AtomEventTypeListItem, AtomEventTypesResponse } from "../types";
+
+export const QUERY_KEY = "use-atom-all-event-types";
+
+export const useAtomGetAllEventTypes = () => {
+ const pathname = `/atoms/${V2_ENDPOINTS.eventTypes}`;
+ const { isInit, accessToken } = useAtomsContext();
+
+ const { isLoading, error, data, refetch } = useQuery({
+ queryKey: [QUERY_KEY],
+ queryFn: () => {
+ return http.get>(pathname).then((res) => {
+ if (res.data.status === SUCCESS_STATUS) {
+ return res.data.data.eventTypes;
+ }
+ throw new Error(res.data.error?.message);
+ });
+ },
+ enabled: isInit && !!accessToken,
+ });
+
+ return { isLoading, error, data, refetch };
+};
diff --git a/packages/platform/atoms/event-types/index.ts b/packages/platform/atoms/event-types/index.ts
new file mode 100644
index 0000000000..e19bd5354b
--- /dev/null
+++ b/packages/platform/atoms/event-types/index.ts
@@ -0,0 +1 @@
+export { ListEventTypesPlatformWrapper } from "./wrappers/ListEventTypesPlatformWrapper";
diff --git a/packages/platform/atoms/event-types/lib/formatEventTypeDuration.ts b/packages/platform/atoms/event-types/lib/formatEventTypeDuration.ts
new file mode 100644
index 0000000000..0e2aa08b7e
--- /dev/null
+++ b/packages/platform/atoms/event-types/lib/formatEventTypeDuration.ts
@@ -0,0 +1,25 @@
+/**
+ * Formats event type duration in a human-readable format.
+ *
+ * @param minutes - Duration in minutes (always an integer for event types)
+ * @returns Formatted duration string (e.g., "30m", "1h", "1h 30m")
+ *
+ * @example
+ * formatEventTypeDuration(30) // "30m"
+ * formatEventTypeDuration(60) // "1h"
+ * formatEventTypeDuration(90) // "1h 30m"
+ */
+export function formatEventTypeDuration(minutes: number): string {
+ if (minutes < 60) {
+ return `${minutes}m`;
+ }
+
+ const hours = Math.floor(minutes / 60);
+ const remainingMinutes = minutes % 60;
+
+ if (remainingMinutes === 0) {
+ return `${hours}h`;
+ }
+
+ return `${hours}h ${remainingMinutes}m`;
+}
diff --git a/packages/platform/atoms/event-types/types.ts b/packages/platform/atoms/event-types/types.ts
new file mode 100644
index 0000000000..2fe1ffdc4d
--- /dev/null
+++ b/packages/platform/atoms/event-types/types.ts
@@ -0,0 +1,16 @@
+/**
+ * Event type item as returned by the atoms API
+ */
+export type AtomEventTypeListItem = {
+ id: number;
+ title: string;
+ slug: string;
+ description: string | null;
+ length: number;
+ locations: unknown | null;
+ logo?: string;
+};
+
+export type AtomEventTypesResponse = {
+ eventTypes: AtomEventTypeListItem[];
+};
diff --git a/packages/platform/atoms/event-types/wrappers/ListEventTypesPlatformWrapper.tsx b/packages/platform/atoms/event-types/wrappers/ListEventTypesPlatformWrapper.tsx
new file mode 100644
index 0000000000..56290943cc
--- /dev/null
+++ b/packages/platform/atoms/event-types/wrappers/ListEventTypesPlatformWrapper.tsx
@@ -0,0 +1,87 @@
+import { AtomsWrapper } from "@/components/atoms-wrapper";
+import { useAutoAnimate } from "@formkit/auto-animate/react";
+
+import { EventTypeListItem } from "../components/EventTypeListItem";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
+import { EmptyScreen } from "@calcom/ui/components/empty-screen";
+
+import { useDeleteEventTypeById } from "../../hooks/event-types/private/useDeleteEventTypeById";
+import { useToast } from "../../src/components/ui/use-toast";
+import { useAtomGetAllEventTypes } from "../hooks/useAtomGetAllEventTypes";
+
+interface ListEventTypesPlatformWrapperProps {
+ getEventTypeUrl?: (eventTypeId: number) => string;
+}
+
+export const ListEventTypesPlatformWrapper = ({
+ getEventTypeUrl,
+}: ListEventTypesPlatformWrapperProps = {}) => {
+ const [animationParentRef] = useAutoAnimate();
+ const { toast } = useToast();
+ const { t } = useLocale();
+
+ const {
+ data: eventTypes,
+ isLoading: isLoadingEventTypes,
+ error,
+ refetch: refetchEventTypes,
+ } = useAtomGetAllEventTypes();
+
+ const { mutate: deleteEventType } = useDeleteEventTypeById({
+ onSuccess: () => {
+ toast({
+ description: t("event_type_deleted_successfully"),
+ });
+ refetchEventTypes();
+ },
+ onError: (err) => {
+ toast({
+ description: err.message,
+ });
+ },
+ });
+
+ if (isLoadingEventTypes) {
+ return <>{t("loading")}>;
+ }
+
+ if (error) {
+ return (
+
+ );
+ }
+
+ if (!isLoadingEventTypes && (!eventTypes || eventTypes.length === 0)) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+ {eventTypes?.map((eventType) => (
+ deleteEventType(eventTypeId)}
+ isDeletable={true}
+ getEventTypeUrl={getEventTypeUrl}
+ />
+ ))}
+
+
+
+ );
+};
diff --git a/packages/platform/atoms/index.ts b/packages/platform/atoms/index.ts
index d3e90ca3fc..6e601cf21d 100644
--- a/packages/platform/atoms/index.ts
+++ b/packages/platform/atoms/index.ts
@@ -49,4 +49,6 @@ export { CreateScheduleForm } from "./create-schedule/CreateScheduleForm";
export { ListSchedulesPlatformWrapper as ListSchedules } from "./list-schedules/index";
+export { ListEventTypesPlatformWrapper as ListEventTypes } from "./event-types/index";
+
export { useAvailableSlots } from "./hooks/useAvailableSlots";