fix: Polish, fixes, and i18n updates for onboarding (#24949)
## What does this PR do? <!-- Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. --> - Fixes #XXXX (GitHub issue number) - Fixes CAL-XXXX (Linear issue number - should be visible at the bottom of the GitHub issue description) ## Visual Demo (For contributors especially) A visual demonstration is strongly recommended, for both the original and new change **(video / image - any one)**. #### Video Demo (if applicable): - Show screen recordings of the issue or feature. - Demonstrate how to reproduce the issue, the behavior before and after the change. #### Image Demo (if applicable): - Add side-by-side screenshots of the original and updated change. - Highlight any significant change(s). ## Mandatory Tasks (DO NOT REMOVE) - [ ] I have self-reviewed the code (A decent size PR without self-review might be rejected). - [ ] I have updated the developer docs in /docs if this PR makes changes that would require a [documentation change](https://cal.com/docs). If N/A, write N/A here and check the checkbox. - [ ] I confirm automated tests are in place that prove my fix is effective or that my feature works. ## How should this be tested? <!-- Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration. Write details that help to start the tests --> - Are there environment variables that should be set? - What are the minimal test data to have? - What is expected (happy path) to have (input and output)? - Any other important info that could help to test that PR ## Checklist <!-- Remove bullet points below that don't apply to you --> - I haven't read the [contributing guide](https://github.com/calcom/cal.com/blob/main/CONTRIBUTING.md) - My code doesn't follow the style guidelines of this project - I haven't commented my code, particularly in hard-to-understand areas - I haven't checked if my changes generate no new warnings <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Polished the onboarding experience with a live calendar preview, a team bio field, and improved username handling. Updated i18n strings and added responsive breakpoints for large screens. - **New Features** - Added right-column previews: OnboardingBrowserView in org teams and a live weekly OnboardingCalendarBrowserView for personal calendar setup. - Introduced team bio in onboarding store and UI. - Expanded i18n: team bio, browser view labels, and CSV invite flow. - Added Tailwind screens for 3xl and 4xl to improve large-display layouts. - **Bug Fixes** - Normalized usernames with slugify on default and change to prevent invalid slugs. - Unified disabled/readonly handling for premium vs. standard username fields; respects external disabled prop and org membership. <sup>Written for commit 7804fad66ee516b25e22fc0d9239f3d29249189a. Summary will update automatically on new commits.</sup> <!-- End of auto-generated description by cubic. -->
This commit is contained in:
@@ -6,6 +6,7 @@ import { Controller, useForm } from "react-hook-form";
|
||||
|
||||
import { useOrgBranding } from "@calcom/features/ee/organizations/context/provider";
|
||||
import { WEBSITE_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/types/server/routers/_app";
|
||||
|
||||
@@ -16,6 +17,7 @@ import type { TRPCClientErrorLike } from "@trpc/client";
|
||||
interface UsernameAvailabilityFieldProps {
|
||||
onSuccessMutation?: () => void;
|
||||
onErrorMutation?: (error: TRPCClientErrorLike<AppRouter>) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
interface ICustomUsernameProps extends UsernameAvailabilityFieldProps {
|
||||
@@ -37,14 +39,19 @@ const UsernameTextfield = dynamic(() => import("./UsernameTextfield").then((m) =
|
||||
});
|
||||
|
||||
export const UsernameAvailability = (props: ICustomUsernameProps) => {
|
||||
const { isPremium, ...otherProps } = props;
|
||||
const { isPremium, disabled, ...otherProps } = props;
|
||||
const UsernameAvailabilityComponent = isPremium ? PremiumTextfield : UsernameTextfield;
|
||||
return <UsernameAvailabilityComponent {...otherProps} />;
|
||||
// PremiumTextfield uses `readonly` prop, UsernameTextfield uses `disabled` prop
|
||||
const componentProps = isPremium
|
||||
? { ...otherProps, readonly: disabled }
|
||||
: { ...otherProps, disabled };
|
||||
return <UsernameAvailabilityComponent {...componentProps} />;
|
||||
};
|
||||
|
||||
export const UsernameAvailabilityField = ({
|
||||
onSuccessMutation,
|
||||
onErrorMutation,
|
||||
disabled,
|
||||
}: UsernameAvailabilityFieldProps) => {
|
||||
const searchParams = useSearchParams();
|
||||
const [user] = trpc.viewer.me.get.useSuspenseQuery();
|
||||
@@ -56,7 +63,7 @@ export const UsernameAvailabilityField = ({
|
||||
: { username: currentUsernameState || "", setQuery: setCurrentUsernameState };
|
||||
const formMethods = useForm({
|
||||
defaultValues: {
|
||||
username: currentUsername,
|
||||
username: slugify(currentUsername || user.username || ""),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,10 +85,14 @@ export const UsernameAvailabilityField = ({
|
||||
setCurrentUsername={setCurrentUsername}
|
||||
inputUsernameValue={value}
|
||||
usernameRef={ref}
|
||||
setInputUsernameValue={onChange}
|
||||
setInputUsernameValue={(val) => {
|
||||
const displayValue = slugify(val, true);
|
||||
formMethods.setValue("username", displayValue);
|
||||
onChange?.(displayValue);
|
||||
}}
|
||||
onSuccessMutation={onSuccessMutation}
|
||||
onErrorMutation={onErrorMutation}
|
||||
disabled={!!user.organization?.id}
|
||||
disabled={disabled ?? !!user.organization?.id}
|
||||
addOnLeading={`${usernamePrefix}/`}
|
||||
isPremium={isPremium}
|
||||
/>
|
||||
|
||||
@@ -1,91 +1,58 @@
|
||||
"use client";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, type IconName } from "@calcom/ui/components/icon";
|
||||
import { useMemo } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import { weekdayDates } from "@calcom/features/calendars/weeklyview/utils";
|
||||
import { CURRENT_TIMEZONE } from "@calcom/lib/timezoneConstants";
|
||||
|
||||
import { useOnboardingCalendarEvents } from "../hooks/useOnboardingCalendarEvents";
|
||||
|
||||
export const OnboardingCalendarBrowserView = () => {
|
||||
const { t } = useLocale();
|
||||
const webappUrl = WEBAPP_URL.replace(/^https?:\/\//, "");
|
||||
const { startDate, endDate } = useMemo(() => {
|
||||
return weekdayDates(0, new Date(), 6);
|
||||
}, []);
|
||||
|
||||
const calendarIntegrations: Array<{
|
||||
name: string;
|
||||
description: string;
|
||||
icon: IconName;
|
||||
}> = [
|
||||
{
|
||||
name: t("google_calendar"),
|
||||
description: t("onboarding_calendar_browser_view_google_description"),
|
||||
icon: "calendar",
|
||||
},
|
||||
{
|
||||
name: t("outlook_calendar"),
|
||||
description: t("onboarding_calendar_browser_view_outlook_description"),
|
||||
icon: "mail",
|
||||
},
|
||||
{
|
||||
name: t("apple_calendar"),
|
||||
description: t("onboarding_calendar_browser_view_apple_description"),
|
||||
icon: "calendar-days",
|
||||
},
|
||||
];
|
||||
const { events } = useOnboardingCalendarEvents({ startDate, endDate });
|
||||
|
||||
// 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]
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="bg-default border-subtle hidden h-full w-full flex-col rounded-l-2xl border xl:flex">
|
||||
{/* Browser header */}
|
||||
<div className="border-subtle flex min-w-0 shrink-0 items-center gap-3 rounded-t-2xl border-b bg-white p-3">
|
||||
{/* Navigation buttons */}
|
||||
<div className="flex shrink-0 items-center gap-4 opacity-50">
|
||||
<Icon name="arrow-left" className="text-subtle h-4 w-4" />
|
||||
<Icon name="arrow-right" className="text-subtle h-4 w-4" />
|
||||
<Icon name="rotate-cw" className="text-subtle h-4 w-4" />
|
||||
</div>
|
||||
<div className="bg-muted flex w-full items-center gap-2 rounded-[32px] px-3 py-2">
|
||||
<Icon name="lock" className="text-subtle h-4 w-4" />
|
||||
<p className="text-default text-sm font-medium leading-tight">{webappUrl}/settings/calendars</p>
|
||||
</div>
|
||||
<Icon name="ellipsis-vertical" className="text-subtle h-4 w-4" />
|
||||
<div className="bg-default border-muted flex h-full w-full flex-col overflow-hidden rounded-xl border">
|
||||
<div className="flex items-center gap-2 px-4 py-3">
|
||||
<span className="text-default text-sm font-semibold">
|
||||
{dayjs(startDate).format("MMM D")}
|
||||
<span className="mx-1">–</span>
|
||||
{dayjs(endDate).format("MMM D, YYYY")}
|
||||
</span>
|
||||
</div>
|
||||
{/* Content */}
|
||||
<div className="bg-muted h-full pl-11 pt-11">
|
||||
<div className="bg-default border-muted flex h-full w-full flex-col overflow-hidden rounded-xl border">
|
||||
{/* Header */}
|
||||
<div className="border-subtle flex flex-col gap-4 border-b p-4">
|
||||
<div className="flex flex-col items-start gap-2">
|
||||
<h2 className="text-emphasis text-xl font-semibold leading-tight">
|
||||
{t("connect_your_calendar")}
|
||||
</h2>
|
||||
<p className="text-subtle text-sm leading-normal">
|
||||
{t("onboarding_calendar_browser_view_subtitle")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Calendar Integrations List */}
|
||||
<div className="flex flex-col overflow-y-auto">
|
||||
{calendarIntegrations.map((integration, index) => (
|
||||
<div key={integration.name} className="opacity-30">
|
||||
{index > 0 && <div className="border-subtle h-px border-t" />}
|
||||
<div className="flex items-center justify-between gap-3 px-5 py-4">
|
||||
<div className="flex min-w-0 flex-1 items-center gap-3">
|
||||
<div className="bg-muted flex h-10 w-10 shrink-0 items-center justify-center rounded-lg">
|
||||
<Icon name={integration.icon} className="text-emphasis h-5 w-5" />
|
||||
</div>
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<h3 className="text-default text-sm font-semibold leading-none">{integration.name}</h3>
|
||||
<p className="text-subtle text-sm font-medium leading-tight">
|
||||
{integration.description}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="bg-emphasis flex h-6 items-center justify-center rounded-md px-2">
|
||||
<span className="text-emphasis text-xs font-medium leading-none">{t("connected")}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{/* Calendar View */}
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<Calendar {...calendarProps} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -10,8 +10,9 @@ import { Button } from "@calcom/ui/components/button";
|
||||
import { Form, TextField } from "@calcom/ui/components/form";
|
||||
import { Icon } from "@calcom/ui/components/icon";
|
||||
|
||||
import { OnboardingCard } from "../../personal/_components/OnboardingCard";
|
||||
import { OnboardingLayout } from "../../personal/_components/OnboardingLayout";
|
||||
import { OnboardingBrowserView } from "../../components/onboarding-browser-view";
|
||||
import { OnboardingCard } from "../../components/OnboardingCard";
|
||||
import { OnboardingLayout } from "../../components/OnboardingLayout";
|
||||
import { useOnboardingStore } from "../../store/onboarding-store";
|
||||
|
||||
type OrganizationTeamsViewProps = {
|
||||
@@ -65,6 +66,7 @@ export const OrganizationTeamsView = ({ userEmail }: OrganizationTeamsViewProps)
|
||||
|
||||
return (
|
||||
<OnboardingLayout userEmail={userEmail} currentStep={3}>
|
||||
{/* Left column - Main content */}
|
||||
<OnboardingCard
|
||||
title={t("onboarding_org_teams_title")}
|
||||
subtitle={t("onboarding_org_teams_subtitle")}
|
||||
@@ -132,6 +134,9 @@ export const OrganizationTeamsView = ({ userEmail }: OrganizationTeamsViewProps)
|
||||
</div>
|
||||
</Form>
|
||||
</OnboardingCard>
|
||||
|
||||
{/* Right column - Browser view */}
|
||||
<OnboardingBrowserView />
|
||||
</OnboardingLayout>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -4,11 +4,11 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button } from "@calcom/ui/components/button";
|
||||
|
||||
import { OnboardingCard } from "../../components/OnboardingCard";
|
||||
import { OnboardingLayout } from "../../components/OnboardingLayout";
|
||||
import { OnboardingCalendarBrowserView } from "../../components/onboarding-calendar-browser-view";
|
||||
import { useSubmitPersonalOnboarding } from "../../hooks/useSubmitPersonalOnboarding";
|
||||
import { InstallableAppCard } from "../_components/InstallableAppCard";
|
||||
import { OnboardingCard } from "../../components/OnboardingCard";
|
||||
import { OnboardingLayout } from "../../components/OnboardingLayout";
|
||||
import { useAppInstallation } from "../_components/useAppInstallation";
|
||||
|
||||
type PersonalCalendarViewProps = {
|
||||
|
||||
@@ -3972,5 +3972,33 @@
|
||||
"repeats_num_times": "Repeats {{count}} times",
|
||||
"view_booking_details": "View Booking Details",
|
||||
"view": "View",
|
||||
"team_bio": "Team Bio",
|
||||
"team_bio_placeholder": "Tell us about your team...",
|
||||
"onboarding_invite_subtitle": "Connect your Google workspace, invite via email, upload a CSV file or copy the invite link and share it with your teammates to add them to your Team.",
|
||||
"upload_csv_subtitle": "Upload a CSV file with team member emails to bulk invite",
|
||||
"need_template": "Need a template?",
|
||||
"download_csv_template_description": "Download our CSV template to ensure your file is formatted correctly",
|
||||
"download_template": "Download template",
|
||||
"upload_your_file": "Upload your file",
|
||||
"upload_csv_description": "Select a CSV file with email addresses and optional roles",
|
||||
"choose_file": "Choose file",
|
||||
"please_upload_csv_file": "Please upload a CSV file",
|
||||
"template_downloaded": "Template downloaded successfully",
|
||||
"please_select_file": "Please select a file to upload",
|
||||
"csv_file_empty": "CSV file is empty or invalid",
|
||||
"csv_missing_email_column": "CSV file must contain an 'email' column",
|
||||
"csv_uploaded_successfully": "Successfully uploaded {{count}} invites",
|
||||
"error_uploading_csv": "Error uploading CSV file",
|
||||
"onboarding_browser_view_demo": "Demo",
|
||||
"onboarding_browser_view_demo_description": "Schedule a demo call with us",
|
||||
"onboarding_browser_view_quick_meeting": "Quick meeting",
|
||||
"onboarding_browser_view_quick_meeting_description": "A quick chat",
|
||||
"onboarding_browser_view_longer_meeting": "Longer meeting",
|
||||
"onboarding_browser_view_longer_meeting_description": "A longer chat",
|
||||
"onboarding_browser_view_in_person_description": "Meet in person",
|
||||
"onboarding_browser_view_ask_question": "Ask a question",
|
||||
"onboarding_browser_view_ask_question_description": "Ask a question",
|
||||
"onboarding_browser_view_default_bio": "Find a time that suits you",
|
||||
"book_now": "Book now",
|
||||
"ADD_NEW_STRINGS_ABOVE_THIS_LINE_TO_PREVENT_MERGE_CONFLICTS": "↑↑↑↑↑↑↑↑↑↑↑↑↑ Add your new strings above here ↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑↑"
|
||||
}
|
||||
|
||||
@@ -220,6 +220,8 @@ module.exports = {
|
||||
},
|
||||
screens: {
|
||||
pwa: { raw: "(display-mode: standalone)" },
|
||||
"3xl": "1920px",
|
||||
"4xl": "2560px",
|
||||
},
|
||||
keyframes: {
|
||||
"fade-in-up": {
|
||||
|
||||
Reference in New Issue
Block a user