Files
calendar/apps/web/modules/settings/teams/new/create-new-team-view.tsx
Eunjae LeeGitHubClaude Opus 4.5Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
81e4241f85 refactor: apply biome formatting to apps/web (#27692)
* refactor: apply biome formatting to apps/web (batch 1)

Formats apps/web non-modules directories:
- apps/web/app
- apps/web/lib
- apps/web/pages
- apps/web/styles
- apps/web/server
- apps/web/test
- Root-level .ts and .mjs files

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to apps/web/modules (batch 2)

Formats smaller apps/web/modules directories:
- onboarding, data-table, users, apps, auth
- integration-attribute-sync, shell, availability
- feature-flags, team, webhooks, getting-started
- feature-opt-in, troubleshooter, schedules, notifications
- signup-view.tsx

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to apps/web/modules (batch 3)

Formats medium apps/web/modules directories:
- bookings
- event-types
- insights
- embed

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to apps/web/modules/ee (batch 4)

Formats apps/web/modules/ee directory.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to apps/web (batch 5)

Formats remaining apps/web directories:
- apps/web/modules/settings
- apps/web/modules/booking-audit
- apps/web/playwright

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to apps/web/components (batch 6)

Formats remaining apps/web/components files.

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to new apps/web files from main

Formats newly added/modified files from main merge:
- WorkflowStepContainer.tsx (resolved merge conflicts)
- Newly moved files (blocklist, form-builder, schedules, etc.)
- Other files modified in main

Co-Authored-By: Claude Opus 4.5 <[email protected]>

* refactor: apply biome formatting to new apps/web files from main

Co-Authored-By: Claude Opus 4.5 <[email protected]>

---------

Co-authored-by: Claude Opus 4.5 <[email protected]>
Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
2026-02-10 23:25:24 +05:30

215 lines
7.5 KiB
TypeScript

"use client";
import { useRouter } from "next/navigation";
import posthog from "posthog-js";
import React, { useEffect, useRef, useState, type FormEvent } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import slugify from "@calcom/lib/slugify";
import { Avatar } from "@calcom/ui/components/avatar";
import { Button } from "@calcom/ui/components/button";
import { Label, TextField, TextArea } from "@calcom/ui/components/form";
import { ImageUploader } from "@calcom/ui/components/image-uploader";
import { showToast } from "@calcom/ui/components/toast";
import { OnboardingCard } from "~/onboarding/components/OnboardingCard";
import { OnboardingLayout } from "~/onboarding/components/OnboardingLayout";
import { OnboardingBrowserView } from "~/onboarding/components/onboarding-browser-view";
import { useCreateTeam } from "~/onboarding/hooks/useCreateTeam";
import { useOnboardingStore } from "~/onboarding/store/onboarding-store";
import { ValidatedTeamSlug } from "~/onboarding/teams/details/validated-team-slug";
type CreateNewTeamViewProps = {
userEmail: string;
};
export const CreateNewTeamView = ({ userEmail }: CreateNewTeamViewProps) => {
const router = useRouter();
const { t } = useLocale();
const store = useOnboardingStore();
const { teamDetails, teamBrand, setTeamDetails, setTeamBrand, resetOnboardingPreservingPlan } = store;
const { createTeam, isSubmitting } = useCreateTeam({
redirectBasePath: "/settings/teams/new",
isOnboarding: false,
});
const logoRef = useRef<HTMLInputElement>(null);
const [teamName, setTeamName] = useState("");
const [teamSlug, setTeamSlug] = useState("");
const [teamBio, setTeamBio] = useState("");
const [teamLogo, setTeamLogo] = useState<string>("");
const [isSlugValid, setIsSlugValid] = useState(false);
const [isSlugManuallyEdited, setIsSlugManuallyEdited] = useState(false);
// Reset onboarding store when entering the team creation flow from settings
useEffect(() => {
resetOnboardingPreservingPlan();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
useEffect(() => {
setTeamName(teamDetails.name);
setTeamSlug(teamDetails.slug);
setTeamBio(teamDetails.bio);
setTeamLogo(teamBrand.logo || "");
if (teamDetails.slug) {
setIsSlugManuallyEdited(true);
}
}, [teamDetails, teamBrand]);
useEffect(() => {
if (!isSlugManuallyEdited && teamName) {
const slugifiedName = slugify(teamName);
setTeamSlug(slugifiedName);
}
}, [teamName, isSlugManuallyEdited]);
const handleSlugChange = (value: string) => {
setTeamSlug(value);
setIsSlugManuallyEdited(true);
};
const handleLogoChange = (newLogo: string) => {
if (logoRef.current) {
logoRef.current.value = newLogo;
}
setTeamLogo(newLogo);
};
const handleContinue = async (e?: FormEvent) => {
e?.preventDefault();
if (!isSlugValid) {
return;
}
posthog.capture("settings_team_details_continue_clicked", {
has_logo: !!teamLogo,
has_bio: !!teamBio,
});
setTeamDetails({
name: teamName,
slug: teamSlug,
bio: teamBio,
});
setTeamBrand({
logo: teamLogo || null,
});
try {
await createTeam();
} catch (error) {
const message = error instanceof Error ? error.message : t("something_went_wrong");
showToast(message, "error");
}
};
const handleCancel = () => {
posthog.capture("settings_team_details_cancel_clicked");
resetOnboardingPreservingPlan();
router.push("/teams");
};
return (
<OnboardingLayout userEmail={userEmail} currentStep={1} totalSteps={3}>
{/* Left column - Main content */}
<div className="flex h-full w-full flex-col gap-4">
<form onSubmit={handleContinue} className="flex h-full w-full flex-col gap-4">
<OnboardingCard
title={t("create_your_team")}
subtitle={t("team_onboarding_details_subtitle")}
footer={
<div className="flex w-full items-center justify-end gap-4">
<Button
type="button"
color="minimal"
className="rounded-[10px]"
onClick={handleCancel}
disabled={isSubmitting}>
{t("cancel")}
</Button>
<Button
type="submit"
color="primary"
className="rounded-[10px]"
disabled={!isSlugValid || !teamName || !teamSlug || isSubmitting}>
{t("continue")}
</Button>
</div>
}>
<div className="flex h-full w-full flex-col gap-4">
{/* Team Profile Picture */}
<div className="flex w-full flex-col gap-2">
<Label className="text-emphasis text-sm font-medium leading-4">{t("team_logo")}</Label>
<div className="flex flex-row items-center justify-start gap-2 rtl:justify-end">
<div className="relative shrink-0">
<Avatar
size="lg"
imageSrc={teamLogo || undefined}
alt={teamName || "Team"}
className="border-2 border-white"
/>
</div>
<input ref={logoRef} type="hidden" name="logo" id="logo" defaultValue={teamLogo} />
<ImageUploader
target="avatar"
id="team-logo-upload"
buttonMsg={t("upload")}
handleAvatarChange={handleLogoChange}
imageSrc={teamLogo}
/>
</div>
<p className="text-subtle text-xs font-normal leading-3">{t("onboarding_logo_size_hint")}</p>
</div>
{/* Team Name */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-0 text-sm font-medium leading-4">{t("team_name")}</Label>
<TextField
noLabel
name="name"
data-testid="team-name-input"
value={teamName}
onChange={(e) => setTeamName(e.target.value)}
placeholder="Acme Inc."
className="border-default h-7 rounded-[10px] border px-2 py-1.5 text-sm"
/>
</div>
{/* Team Slug */}
<ValidatedTeamSlug
value={teamSlug}
onChange={handleSlugChange}
onValidationChange={setIsSlugValid}
/>
{/* Team Bio */}
<div className="flex w-full flex-col gap-1.5">
<Label className="text-emphasis mb-0 text-sm font-medium leading-4">{t("team_bio")}</Label>
<TextArea
value={teamBio}
onChange={(e) => setTeamBio(e.target.value)}
placeholder={t("team_bio_placeholder")}
className="border-default max-h-[150px] min-h-[80px] rounded-[10px] border px-2 py-1.5 text-sm"
rows={3}
/>
</div>
</div>
</OnboardingCard>
</form>
</div>
{/* Right column - Browser view */}
<OnboardingBrowserView teamSlug={teamSlug} name={teamName} bio={teamBio} avatar={teamLogo || null} />
</OnboardingLayout>
);
};
export const LayoutWrapper = ({ children }: { children: React.ReactNode }) => {
return <>{children}</>;
};
export default CreateNewTeamView;