Blocksblock
Create organization
Name and URL slug form for a new organization.
Preview
Open full screenInstallation
npx shadcn@latest add @particular/create-organizationUsage
"use client"
import { CreateOrganizationForm } from "@/components/create-organization-form"
import { toast } from "@/components/ui/toast"
function wait(ms: number) {
return new Promise<void>((resolve) => setTimeout(resolve, ms))
}
export function Demo() {
return (
<div className="flex min-h-svh items-center justify-center bg-muted/30 p-6">
<div className="w-full max-w-md rounded-2xl border bg-card p-8 shadow-sm">
<h1 className="mb-1 font-heading text-xl font-semibold tracking-tight">Create an organization</h1>
<p className="mb-6 text-sm text-muted-foreground">Organizations own API keys and members, separate from your personal account.</p>
<CreateOrganizationForm
slugPrefix="particular.tech/"
onCancel={() => toast.add({ title: "Cancelled" })}
onCreate={async ({ name, slug }) => {
await wait(800)
if (slug === "acme") throw new Error("That URL is taken. Choose another.")
toast.add({ title: `Created ${name}`, type: "success" })
}}
/>
</div>
</div>
)
}Source
"use client"
import * as React from "react"
import { cn } from "cn"
import { Alert, AlertDescription } from "@/components/ui/alert"
import { Button } from "@/components/ui/button"
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field"
import { Input } from "@/components/ui/input"
import { InputGroup, InputGroupAddon, InputGroupInput, InputGroupText } from "@/components/ui/input-group"
import { Spinner } from "@/components/ui/spinner"
export type CreateOrganizationFormProps = {
/** Resolve once created; the caller usually navigates to the new organization. Reject with an Error to show its message. */
onCreate: (organization: { name: string; slug: string }) => Promise<void>
/** Shown before the slug, e.g. `particular.tech/`. */
slugPrefix?: string
onCancel?: () => void
className?: string
}
const SLUG_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/
function slugify(name: string) {
return name
.normalize("NFKD")
.replace(/[\u0300-\u036f]/g, "")
.toLowerCase()
.replace(/[^a-z0-9]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 39)
.replace(/-+$/, "")
}
function CreateOrganizationForm({ onCreate, slugPrefix, onCancel, className }: CreateOrganizationFormProps) {
const [name, setName] = React.useState("")
const [slug, setSlug] = React.useState("")
const [slugEdited, setSlugEdited] = React.useState(false)
const [pending, setPending] = React.useState(false)
const [error, setError] = React.useState<string | null>(null)
const [slugError, setSlugError] = React.useState<string | null>(null)
const submit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
const trimmed = name.trim()
if (!trimmed) return setError("Give the organization a name.")
if (!SLUG_PATTERN.test(slug)) {
return setSlugError("Use 1–39 lowercase letters, numbers, or hyphens, starting and ending with a letter or number.")
}
setPending(true)
setError(null)
setSlugError(null)
try {
await onCreate({ name: trimmed, slug })
} catch (caught) {
setError(caught instanceof Error ? caught.message : "Something went wrong. Try again.")
} finally {
setPending(false)
}
}
return (
<form onSubmit={submit} className={cn("flex w-full max-w-md flex-col gap-5", className)}>
<Field>
<FieldLabel htmlFor="organization-name">Organization name</FieldLabel>
<Input
id="organization-name"
name="name"
value={name}
autoComplete="organization"
autoFocus
placeholder="Acme Inc."
onChange={(event) => {
setName(event.target.value)
if (!slugEdited) setSlug(slugify(event.target.value))
}}
/>
</Field>
<Field data-invalid={slugError ? true : undefined}>
<FieldLabel htmlFor="organization-slug">URL</FieldLabel>
<InputGroup>
{slugPrefix && (
<InputGroupAddon>
<InputGroupText className="font-mono text-xs">{slugPrefix}</InputGroupText>
</InputGroupAddon>
)}
<InputGroupInput
id="organization-slug"
name="slug"
value={slug}
autoComplete="off"
spellCheck={false}
className="font-mono text-sm"
aria-invalid={slugError ? true : undefined}
onChange={(event) => {
setSlugEdited(true)
setSlug(event.target.value.toLowerCase())
}}
/>
</InputGroup>
{slugError ? <FieldError>{slugError}</FieldError> : <FieldDescription>Lowercase letters, numbers, and hyphens. You can change it later.</FieldDescription>}
</Field>
{error && (
<Alert variant="destructive">
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<div className="flex justify-end gap-2">
{onCancel && (
<Button type="button" variant="outline" disabled={pending} onClick={onCancel}>
Cancel
</Button>
)}
<Button type="submit" disabled={pending}>
{pending && <Spinner data-icon="inline-start" />}
Create organization
</Button>
</div>
</form>
)
}
export { CreateOrganizationForm }