From 645a3448911b984abb0e4857e1c0eedaa1ce1915 Mon Sep 17 00:00:00 2001 From: dumibell Date: Tue, 16 Dec 2025 13:52:52 +0700 Subject: [PATCH 1/6] Add create org to onabording flow --- src/app/create-org/page.tsx | 23 ++++ src/app/onboarding/create-org/page.tsx | 19 +++ src/app/onboarding/{ => pricing}/page.tsx | 4 - src/app/settings/org/create-org.tsx | 79 ------------- src/app/sign-up/page.tsx | 2 +- src/components/onboarding/pricing.tsx | 6 +- src/components/organization/create-org.tsx | 129 +++++++++++++++++++++ 7 files changed, 173 insertions(+), 89 deletions(-) create mode 100644 src/app/create-org/page.tsx create mode 100644 src/app/onboarding/create-org/page.tsx rename src/app/onboarding/{ => pricing}/page.tsx (78%) delete mode 100644 src/app/settings/org/create-org.tsx create mode 100644 src/components/organization/create-org.tsx diff --git a/src/app/create-org/page.tsx b/src/app/create-org/page.tsx new file mode 100644 index 00000000..4e4446ac --- /dev/null +++ b/src/app/create-org/page.tsx @@ -0,0 +1,23 @@ +"use client"; + +import { CreateOrg } from "@/components/organization/create-org"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth-client"; + +export default function CreateOrgPage() { + const router = useRouter(); + const { useActiveOrganization } = authClient; + const { data: activeOrganization } = useActiveOrganization(); + + const onSuccess = () => { + toast.success("Organization created successfully"); + router.push(`/${activeOrganization?.slug}/localhost/query`); + }; + + return ( +
+ +
+ ); +} diff --git a/src/app/onboarding/create-org/page.tsx b/src/app/onboarding/create-org/page.tsx new file mode 100644 index 00000000..68a44a68 --- /dev/null +++ b/src/app/onboarding/create-org/page.tsx @@ -0,0 +1,19 @@ +"use client"; + +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; +import { CreateOrg } from "@/components/organization/create-org"; + +export default function OnboardingCreateOrgPage() { + const router = useRouter(); + + const onSuccess = () => { + toast.success("Organization created successfully"); + router.push("/onboarding/pricing"); + }; + return ( +
+ +
+ ); +} diff --git a/src/app/onboarding/page.tsx b/src/app/onboarding/pricing/page.tsx similarity index 78% rename from src/app/onboarding/page.tsx rename to src/app/onboarding/pricing/page.tsx index b296e896..6be04a64 100644 --- a/src/app/onboarding/page.tsx +++ b/src/app/onboarding/pricing/page.tsx @@ -1,12 +1,8 @@ "use client"; -import { useSearchParams } from "next/navigation"; import OnboardingPricing from "@/components/onboarding/pricing"; export default function OnboardingPage() { - const searchParams = useSearchParams(); - const step = searchParams.get("step"); - return (
diff --git a/src/app/settings/org/create-org.tsx b/src/app/settings/org/create-org.tsx deleted file mode 100644 index 763ce58f..00000000 --- a/src/app/settings/org/create-org.tsx +++ /dev/null @@ -1,79 +0,0 @@ -"use client"; - -import { Button } from "@/components/ui/button"; -import { - Card, - CardHeader, - CardTitle, - CardDescription, - CardContent, - CardFooter, -} from "@/components/ui/card"; -import { Input } from "@/components/ui/input"; -import { authClient } from "@/lib/auth-client"; -import { Loader2 } from "lucide-react"; -import { useState } from "react"; -import { toast } from "sonner"; - -export const CreateOrg = () => { - const { organization, useSession } = authClient; - const { data: session } = useSession(); - const [orgName, setOrgName] = useState(""); - const [isLoading, setIsLoading] = useState(false); - - const handleCreateOrg = async () => { - setIsLoading(true); - await organization.create( - { - name: orgName, - slug: orgName, - userId: session?.user.id, - metadata: { - createdBy: session?.user.id, - }, - }, - { - onSuccess: () => { - toast.success("Organization created successfully"); - setIsLoading(false); - }, - onError: (error) => { - toast.error(`Failed to create organization: ${error.error.message}`); - console.error(error); - setIsLoading(false); - }, - }, - ); - }; - - return ( - - - Create New Organization - - Create a new organization and become its owner - - - - setOrgName(e.target.value)} - /> - - - - - - ); -}; diff --git a/src/app/sign-up/page.tsx b/src/app/sign-up/page.tsx index 4f1c253c..6a672b26 100644 --- a/src/app/sign-up/page.tsx +++ b/src/app/sign-up/page.tsx @@ -120,7 +120,7 @@ export default function SignUpPage() { password, name: username, username, - callbackURL: "/sign-up/success", + callbackURL: "/onboarding/create-org", }, { onRequest: (ctx) => { diff --git a/src/components/onboarding/pricing.tsx b/src/components/onboarding/pricing.tsx index 46d7f34c..dfb16915 100644 --- a/src/components/onboarding/pricing.tsx +++ b/src/components/onboarding/pricing.tsx @@ -32,11 +32,7 @@ export default function OnboardingPricing() { onSuccess: () => { toast.success("You're all set with the free plan."); if (!activeOrganization || !activeEnvironment) return; - const url = getOrgEnvUrl( - activeOrganization, - activeEnvironment, - activePage, - ); + const url = `${activeOrganization?.slug}/localhost/query`; router.push(url); }, onError: (error) => { diff --git a/src/components/organization/create-org.tsx b/src/components/organization/create-org.tsx new file mode 100644 index 00000000..a4d53e51 --- /dev/null +++ b/src/components/organization/create-org.tsx @@ -0,0 +1,129 @@ +"use client"; + +import { useRouter } from "next/navigation"; +import { authClient } from "@/lib/auth-client"; +import { + Form, + FormLabel, + FormField, + FormItem, + FormControl, + FormMessage, +} from "@/components/ui/form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "node_modules/react-hook-form"; +import { Input } from "@/components/ui/input"; +import { Button } from "@/components/ui/button"; +import { Separator } from "@/components/ui/separator"; +import { toast } from "sonner"; +import { useState } from "react"; +import { Loader2 } from "lucide-react"; + +const FormSchema = z.object({ + name: z.string().min(1, "Organization name is required."), +}); +type FormData = z.infer; + +interface CreateOrgProps { + title: string; + onSuccess: () => void; +} + +export const CreateOrg = ({ title, onSuccess }: CreateOrgProps) => { + const router = useRouter(); + const { organization, useSession } = authClient; + const { data: session } = useSession(); + + const [isCreatingOrg, setIsCreatingOrg] = useState(false); + + const form = useForm({ + resolver: zodResolver(FormSchema), + defaultValues: { + name: "", + }, + }); + + const onSubmit = async (data: FormData) => { + setIsCreatingOrg(true); + const { name } = data; + await organization.create( + { + name, + slug: name, + userId: session?.user.id, + keepCurrentActiveOrganization: false, + metadata: { + createdBy: session?.user.id, + }, + }, + { + onSuccess: () => { + onSuccess(); + setIsCreatingOrg(false); + }, + onError: (error) => { + toast.error(`Failed to create organization: ${error.error.message}`); + setIsCreatingOrg(false); + }, + }, + ); + }; + + return ( +
+
+

{title}

+

+ Set up the workspace where your team will manage services and + environments. +

+
+
+ + ( + + Organization name + + + +
+ +
+
+ )} + /> + + + +
+
+
+ + Or + +
+
+

+ Join existing organization +

+

+ Please contact an administrator of the existing
+ organization and request an invitation. +

+
+
+ ); +}; From a992afec08045d5dd43a50efcdf0ccd1051ffabe Mon Sep 17 00:00:00 2001 From: dumibell Date: Tue, 16 Dec 2025 13:54:25 +0700 Subject: [PATCH 2/6] Improve org pesistency - Prevent deletion of last organization - Persist last active org in localstorage --- src/app/settings/org/org-setting-form.tsx | 58 +++++++++++++---------- src/app/settings/org/page.tsx | 53 +++++---------------- src/components/header/index.tsx | 20 +++++++- src/components/header/org-switcher.tsx | 5 ++ src/stores/organization-store.ts | 21 ++++++++ 5 files changed, 88 insertions(+), 69 deletions(-) create mode 100644 src/stores/organization-store.ts diff --git a/src/app/settings/org/org-setting-form.tsx b/src/app/settings/org/org-setting-form.tsx index a945bd61..6e4e25ab 100644 --- a/src/app/settings/org/org-setting-form.tsx +++ b/src/app/settings/org/org-setting-form.tsx @@ -36,6 +36,7 @@ import { } from "@/components/ui/select"; import { Member, Invitation, Organization } from "better-auth/plugins"; import { User } from "better-auth"; +import { useRouter } from "next/navigation"; interface Role { value: "owner" | "admin" | "member"; @@ -74,7 +75,13 @@ export const OrgSettingForm = ({ activeOrganization, user, }: OrgSettingFormProps) => { - const { organization } = authClient; + const router = useRouter(); + const { organization, useListOrganizations } = authClient; + const { data: allOrganizations } = useListOrganizations(); + + const isLastOrganization = !!( + allOrganizations && allOrganizations.length <= 1 + ); const [email, setEmail] = useState(""); const [orgName, setOrgName] = useState(""); @@ -208,14 +215,33 @@ export const OrgSettingForm = ({ }; const handleDeleteOrg = async () => { + if (isLastOrganization) { + toast.error( + "Cannot delete your last organization. Create a new organization before deleting this one.", + ); + return; + } setIsDeletingOrg(true); await organization.delete( { organizationId: activeOrganization?.id, }, { - onSuccess: () => { + onSuccess: async () => { toast.success("Organization deleted successfully"); + + const remainingOrgs = allOrganizations?.filter( + (org) => org.id !== activeOrganization?.id, + ); + + if (!remainingOrgs || remainingOrgs.length === 0) return; + + await organization.setActive({ + organizationId: remainingOrgs[0].id, + }); + toast.info(`Switched to ${remainingOrgs[0].name}`); + router.refresh(); + setIsDeletingOrg(false); }, onError: (error) => { @@ -534,28 +560,6 @@ export const OrgSettingForm = ({ } - {/* Create New Organization */} - - - Create New Organization - - Create a new organization and become its owner - - - -
- setOrgName(e.target.value)} - /> - -
-
-
- {/* Danger Zone */} { @@ -573,14 +577,16 @@ export const OrgSettingForm = ({ Delete Organization

- Permanently delete this organization and all its data + {isLastOrganization + ? "You must have at least one organization. Create another organization before deleting this one." + : "Permanently delete this organization and all its data"}

-

- Join existing organization -

+

Join existing organization

Please contact an administrator of the existing
organization and request an invitation. From 5671b3d7df4beefc22f4ec4f4619e95d9ec57117 Mon Sep 17 00:00:00 2001 From: dumibell Date: Tue, 16 Dec 2025 14:10:52 +0700 Subject: [PATCH 4/6] Add new organization creation to org switcher --- src/components/header/org-switcher.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/components/header/org-switcher.tsx b/src/components/header/org-switcher.tsx index 7d11263e..6aaf4231 100644 --- a/src/components/header/org-switcher.tsx +++ b/src/components/header/org-switcher.tsx @@ -1,4 +1,4 @@ -import { usePathname } from "next/navigation"; +import { usePathname, useRouter } from "next/navigation"; import { useEffect, useState } from "react"; import { Select, @@ -25,6 +25,7 @@ interface OrgSwitcherProps { } export const OrgSwitcher = ({ user }: OrgSwitcherProps) => { + const router = useRouter(); const pathname = usePathname(); const { activeEnvironment } = useEnvironmentStore(); const { useListOrganizations, organization, useActiveOrganization } = @@ -42,6 +43,11 @@ export const OrgSwitcher = ({ user }: OrgSwitcherProps) => { const selected = menuList.find((menu) => menu.value === value); if (!selected) return; + if (value === "add-new") { + router.push("/create-org"); + return; + } + organization.setActive({ organizationId: value, }); @@ -58,6 +64,11 @@ export const OrgSwitcher = ({ user }: OrgSwitcherProps) => { }); }); + _menuList.push({ + name: "+ Add new", + value: "add-new", + }); + setMenuList(_menuList); }, [listOrganizations, user, activeEnvironment, pathname]); From b001ae1b30a13818e2c0f68a777c4540c4d7614a Mon Sep 17 00:00:00 2001 From: dumibell Date: Tue, 16 Dec 2025 16:06:33 +0700 Subject: [PATCH 5/6] Show error message when failing to create an org --- src/components/organization/create-org.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/components/organization/create-org.tsx b/src/components/organization/create-org.tsx index 5fe787d5..14d26b50 100644 --- a/src/components/organization/create-org.tsx +++ b/src/components/organization/create-org.tsx @@ -44,6 +44,8 @@ export const CreateOrg = ({ title, onSuccess }: CreateOrgProps) => { }, }); + const { setError } = form; + const onSubmit = async (data: FormData) => { setIsCreatingOrg(true); const { name } = data; @@ -63,7 +65,9 @@ export const CreateOrg = ({ title, onSuccess }: CreateOrgProps) => { setIsCreatingOrg(false); }, onError: (error) => { - toast.error(`Failed to create organization: ${error.error.message}`); + const { message } = error.error; + setError("name", { message }); + toast.error(`Failed to create organization: ${message}`); setIsCreatingOrg(false); }, }, From b0845c465b64b550657ff395327e67ddd8ecf063 Mon Sep 17 00:00:00 2001 From: dumibell Date: Tue, 16 Dec 2025 16:16:48 +0700 Subject: [PATCH 6/6] Comment out pricing onboarding step --- src/app/onboarding/create-org/page.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/app/onboarding/create-org/page.tsx b/src/app/onboarding/create-org/page.tsx index 68a44a68..ea19fd45 100644 --- a/src/app/onboarding/create-org/page.tsx +++ b/src/app/onboarding/create-org/page.tsx @@ -3,13 +3,19 @@ import { toast } from "sonner"; import { useRouter } from "next/navigation"; import { CreateOrg } from "@/components/organization/create-org"; +import { authClient } from "@/lib/auth-client"; export default function OnboardingCreateOrgPage() { + const { useActiveOrganization } = authClient; + const { data: activeOrganization } = useActiveOrganization(); const router = useRouter(); const onSuccess = () => { toast.success("Organization created successfully"); - router.push("/onboarding/pricing"); + router.push(`/${activeOrganization?.slug}/localhost/query`); + + // TODO + // router.push("/onboarding/pricing"); }; return (