-
- {/* Profile Header */}
-
-
-
-
-
- {name || t("your_name")}
-
-
- {bio || t("onboarding_browser_view_default_bio")}
-
-
-
-
-
- {/* Events List */}
-
- {events.map((event, index) => (
-
- {index > 0 &&
}
-
-
-
-
{event.title}
-
-
-
- {event.duration} {t("minute_timeUnit")}
-
-
-
-
{event.description}
-
-
+
+
+ {/* Profile Header */}
+
+
+
+
+
+ {name || t("your_name")}
+
+
+ {bio || t("onboarding_browser_view_default_bio")}
+
- ))}
-
-
+
+
+ {/* Events List */}
+
+ {events.map((event, index) => (
+
+ {index > 0 &&
}
+
+
+
+
{event.title}
+
+
+
+ {event.duration} {t("minute_timeUnit")}
+
+
+
+
{event.description}
+
+
+
+
+ ))}
+
+
+
);
diff --git a/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx
index 085e67eaad..32fdafe0d7 100644
--- a/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx
+++ b/apps/web/modules/onboarding/components/onboarding-calendar-browser-view.tsx
@@ -1,59 +1,109 @@
"use client";
+import { AnimatePresence, motion } from "framer-motion";
+import { usePathname } from "next/navigation";
import { useMemo } from "react";
import dayjs from "@calcom/dayjs";
import { Calendar } from "@calcom/features/calendars/weeklyview";
+import type { CalendarComponentProps, Hours } from "@calcom/features/calendars/weeklyview/types/state";
import { weekdayDates } from "@calcom/features/calendars/weeklyview/utils";
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
import { useOnboardingCalendarEvents } from "../hooks/useOnboardingCalendarEvents";
+// Helper function to ensure a number is a valid Hours type
+const toHours = (value: number): Hours => {
+ const clamped = Math.max(0, Math.min(23, value));
+ return clamped as Hours;
+};
+
export const OnboardingCalendarBrowserView = () => {
+ const pathname = usePathname();
const { startDate, endDate } = useMemo(() => {
return weekdayDates(0, new Date(), 6);
}, []);
const { events } = useOnboardingCalendarEvents({ startDate, endDate });
+ // Calculate startHour and endHour based on current time
+ const hours = useMemo((): { startHour: Hours; endHour: Hours } => {
+ const currentHour = dayjs().tz(CURRENT_TIMEZONE).hour();
+ return {
+ startHour: toHours(Math.max(0, currentHour - 2)),
+ endHour: toHours(Math.min(23, currentHour + 6)),
+ };
+ }, []);
+
+ // Animation variants for entry and exit
+ const containerVariants = {
+ initial: {
+ opacity: 0,
+ y: -20,
+ },
+ animate: {
+ opacity: 1,
+ y: 0,
+ },
+ exit: {
+ opacity: 0,
+ y: 20,
+ },
+ };
+
// Memoize calendar props to prevent unnecessary re-initializations
const calendarProps = useMemo(
- () => ({
- timezone: CURRENT_TIMEZONE,
- startDate,
- endDate,
- events: events || [],
- gridCellsPerHour: 4,
- hoverEventDuration: 0,
- showBackgroundPattern: false,
- showBorder: false,
- borderColor: "subtle" as const,
- hideHeader: true,
- eventsDisabled: true,
- sortEvents: true,
- isPending: false, // Explicitly set to false to prevent loading spinner
- loading: false, // Also set loading to false just in case
- onEventClick: () => {},
- onEmptyCellClick: () => {},
- onDateChange: () => {},
- showTimezone: true,
- }),
- [startDate, endDate, events]
+ () =>
+ ({
+ timezone: CURRENT_TIMEZONE,
+ startDate,
+ endDate,
+ events: events || [],
+ startHour: hours.startHour,
+ endHour: hours.endHour,
+ gridCellsPerHour: 4,
+ hoverEventDuration: 0,
+ showBackgroundPattern: false,
+ showBorder: false,
+ borderColor: "subtle" as const,
+ hideHeader: true,
+ eventsDisabled: true,
+ sortEvents: true,
+ isPending: false, // Explicitly set to false to prevent loading spinner
+ loading: false, // Also set loading to false just in case
+ onEventClick: () => {},
+ onEmptyCellClick: () => {},
+ onDateChange: () => {},
+ showTimezone: true,
+ } satisfies CalendarComponentProps),
+ [startDate, endDate, events, hours]
);
return (
-
-
-
- {dayjs(startDate).format("MMM D")}
- –
- {dayjs(endDate).format("MMM D, YYYY")}
-
-
- {/* Calendar View */}
-
-
-
-
+
+
+
+
+ {dayjs(startDate).format("MMM D")}
+ –
+ {dayjs(endDate).format("MMM D, YYYY")}
+
+
+ {/* Calendar View */}
+
+
+
+
+
);
};
diff --git a/apps/web/modules/onboarding/components/onboarding-continuation-prompt.tsx b/apps/web/modules/onboarding/components/onboarding-continuation-prompt.tsx
index fc505ee612..fa8ac03d8b 100644
--- a/apps/web/modules/onboarding/components/onboarding-continuation-prompt.tsx
+++ b/apps/web/modules/onboarding/components/onboarding-continuation-prompt.tsx
@@ -3,6 +3,7 @@
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
+import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/components/button";
import { Icon } from "@calcom/ui/components/icon";
@@ -10,22 +11,50 @@ import { useOnboardingStore } from "../store/onboarding-store";
export const OnboardingContinuationPrompt = () => {
const router = useRouter();
- const { organizationDetails, resetOnboarding } = useOnboardingStore();
+ const { t } = useLocale();
+ const { selectedPlan, organizationDetails, teamDetails, resetOnboarding } = useOnboardingStore();
const [isVisible, setIsVisible] = useState(false);
+ const [entityName, setEntityName] = useState
("");
+ const [entityType, setEntityType] = useState<"organization" | "team" | null>(null);
useEffect(() => {
- // Check if there's existing organization data
- const hasExistingData = organizationDetails.name && organizationDetails.link;
- setIsVisible(!!hasExistingData);
- }, [organizationDetails]);
+ // Check for organization plan and data
+ const hasOrganizationPlan = selectedPlan === "organization";
+ const hasOrganizationData = organizationDetails.name?.trim() && organizationDetails.link?.trim();
- if (!isVisible) {
+ // Check for team plan and data
+ const hasTeamPlan = selectedPlan === "team";
+ const hasTeamData = teamDetails.name?.trim() && teamDetails.slug?.trim();
+
+ // Show if either organization or team has data
+ if (hasOrganizationPlan && hasOrganizationData) {
+ setIsVisible(true);
+ setEntityName(organizationDetails.name);
+ setEntityType("organization");
+ } else if (hasTeamPlan && hasTeamData) {
+ setIsVisible(true);
+ setEntityName(teamDetails.name);
+ setEntityType("team");
+ } else {
+ setIsVisible(false);
+ setEntityName("");
+ setEntityType(null);
+ }
+ }, [selectedPlan, organizationDetails, teamDetails]);
+
+ if (!isVisible || !entityName || !entityType) {
return null;
}
const handleContinue = () => {
- // Navigate to the next step in the flow (brand page)
- router.push("/onboarding/organization/brand");
+ // Navigate to the next step based on plan type
+ if (entityType === "organization") {
+ // Organization flow: details -> brand -> teams -> invite
+ router.push("/onboarding/organization/brand");
+ } else if (entityType === "team") {
+ // Team flow: details -> invite
+ router.push("/onboarding/teams/invite");
+ }
};
const handleStartOver = () => {
@@ -43,19 +72,20 @@ export const OnboardingContinuationPrompt = () => {
-
Continue onboarding?
+
+ {t("onboarding_continue_prompt_title")}
+
- Would you like to carry on with onboarding for{" "}
- {organizationDetails.name} or start over?
+ {t("onboarding_continue_prompt_description", { entityName: entityName })}
diff --git a/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx
index e9ce7747bc..718a766800 100644
--- a/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx
+++ b/apps/web/modules/onboarding/components/onboarding-invite-browser-view.tsx
@@ -1,66 +1,183 @@
"use client";
+import { AnimatePresence, motion } from "framer-motion";
+import { usePathname } from "next/navigation";
+
+import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Avatar } from "@calcom/ui/components/avatar";
-import { useOnboardingStore } from "../store/onboarding-store";
+import { useOnboardingStore, type Invite } from "../store/onboarding-store";
type OnboardingInviteBrowserViewProps = {
teamName?: string;
+ /** If true, use organization invites instead of team invites */
+ useOrganizationInvites?: boolean;
+ /** Optional: Watched form invites to display in real-time. Takes precedence over store values. */
+ watchedInvites?: Array<{
+ email: string;
+ team?: string;
+ role?: string;
+ }>;
};
-export const OnboardingInviteBrowserView = ({ teamName }: OnboardingInviteBrowserViewProps) => {
+type DisplayItem = {
+ name: string;
+ email: string;
+ team: string;
+ isReal?: boolean;
+};
+
+export const OnboardingInviteBrowserView = ({
+ teamName,
+ useOrganizationInvites = false,
+ watchedInvites,
+}: OnboardingInviteBrowserViewProps) => {
+ const pathname = usePathname();
const { data: user } = trpc.viewer.me.get.useQuery();
- const { teamBrand } = useOnboardingStore();
+ const { teamBrand, teamInvites, invites, teamDetails, organizationBrand, organizationDetails } =
+ useOnboardingStore();
+ const { t } = useLocale();
+
+ // Animation variants for entry and exit
+ const containerVariants = {
+ initial: {
+ opacity: 0,
+ y: -20,
+ },
+ animate: {
+ opacity: 1,
+ y: 0,
+ },
+ exit: {
+ opacity: 0,
+ y: 20,
+ },
+ };
// Use default values if not provided
const rawInviterName = user?.name || user?.username || "Alex";
const displayInviterName = rawInviterName.charAt(0).toUpperCase() + rawInviterName.slice(1);
- const displayTeamName = teamName || "Deel";
- const teamAvatar = teamBrand.logo || null;
+
+ // Use organization or team data based on context
+ const displayName = useOrganizationInvites
+ ? organizationDetails.name || teamName || "Deel"
+ : teamName || teamDetails.name || "Deel";
+ const avatar = useOrganizationInvites ? organizationBrand.logo || null : teamBrand.logo || null;
+
+ // Get invites based on context - use watched invites if provided, otherwise fall back to store
+ let actualInvites: Invite[] = [];
+ if (watchedInvites) {
+ // Transform watched invites to Invite format
+ actualInvites = watchedInvites.map((invite) => ({
+ email: invite.email,
+ team: invite.team || (useOrganizationInvites ? "" : teamDetails.name),
+ role: (invite.role as Invite["role"]) || "MEMBER",
+ }));
+ } else {
+ // Fall back to store values
+ actualInvites = useOrganizationInvites ? invites : teamInvites;
+ }
+
+ // Filter out empty invites (where email is empty or just whitespace)
+ const validInvites = actualInvites.filter((invite) => invite.email && invite.email.trim().length > 0);
+
+ // Create empty state items
+ const emptyStateItem = {
+ name: t("team_member"),
+ email: "name@example.com",
+ team: t("team"),
+ };
+
+ // Combine actual invites with empty state to fill up to 9 items
+
+ const displayItems: DisplayItem[] = [];
+ const maxItems = 9;
+
+ // Add actual invites first
+ for (let i = 0; i < validInvites.length && i < maxItems; i++) {
+ const invite = validInvites[i];
+ displayItems.push({
+ name: invite.email.split("@")[0] || t("team_member"),
+ email: invite.email,
+ team: invite.team || t("team"),
+ isReal: true,
+ });
+ }
+
+ // Fill remaining slots with empty state
+ const remainingSlots = maxItems - displayItems.length;
+ for (let i = 0; i < remainingSlots; i++) {
+ displayItems.push({
+ name: emptyStateItem.name,
+ email: emptyStateItem.email,
+ team: emptyStateItem.team,
+ isReal: false,
+ });
+ }
return (
-