mirror of
https://github.com/hanzoai/calendar.git
synced 2026-10-30 14:46:43 +00:00
* 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>
215 lines
7.5 KiB
TypeScript
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;
|