112 lines
3.5 KiB
TypeScript
112 lines
3.5 KiB
TypeScript
import * as React from "react";
|
|
import { useNavigate } from "@tanstack/react-router";
|
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { toast } from "sonner";
|
|
import { Modal } from "@/client/components/Modal";
|
|
import { getStandardErrorMessage } from "@/client/lib/error-messages";
|
|
import { setLastProjectId } from "@/client/lib/active-project";
|
|
import { createProject } from "@/serverFunctions/projects";
|
|
|
|
export function CreateProjectModal({ onClose }: { onClose: () => void }) {
|
|
const navigate = useNavigate();
|
|
const queryClient = useQueryClient();
|
|
const [name, setName] = React.useState("");
|
|
const [domain, setDomain] = React.useState("");
|
|
|
|
const createMutation = useMutation({
|
|
mutationFn: () =>
|
|
createProject({
|
|
data: { name: name.trim(), domain: domain.trim() || undefined },
|
|
}),
|
|
onSuccess: async (created) => {
|
|
setLastProjectId(created.id);
|
|
await queryClient.invalidateQueries({ queryKey: ["projects"] });
|
|
onClose();
|
|
toast.success("Project created");
|
|
// Land on the new project's settings so they can connect Search Console
|
|
// and finish setting up the workspace.
|
|
void navigate({
|
|
to: "/p/$projectId/settings",
|
|
params: { projectId: created.id },
|
|
});
|
|
},
|
|
onError: (error) =>
|
|
toast.error(getStandardErrorMessage(error, "Failed to create project")),
|
|
});
|
|
|
|
const isPending = createMutation.isPending;
|
|
|
|
const handleSubmit = (event: React.FormEvent) => {
|
|
event.preventDefault();
|
|
if (isPending) return;
|
|
if (!name.trim()) {
|
|
toast.error("Project name is required");
|
|
return;
|
|
}
|
|
createMutation.mutate();
|
|
};
|
|
|
|
return (
|
|
<Modal
|
|
maxWidth="max-w-md"
|
|
onClose={isPending ? undefined : onClose}
|
|
labelledBy="create-project-title"
|
|
>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<h2 id="create-project-title" className="text-lg font-semibold">
|
|
New project
|
|
</h2>
|
|
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="font-medium">Name</span>
|
|
<input
|
|
type="text"
|
|
value={name}
|
|
onChange={(event) => setName(event.target.value)}
|
|
placeholder="Acme Inc."
|
|
maxLength={120}
|
|
autoFocus
|
|
className="input input-bordered w-full"
|
|
/>
|
|
</label>
|
|
|
|
<label className="flex flex-col gap-1.5 text-sm">
|
|
<span className="font-medium">
|
|
Domain <span className="text-base-content/50">(optional)</span>
|
|
</span>
|
|
<input
|
|
type="text"
|
|
value={domain}
|
|
onChange={(event) => setDomain(event.target.value)}
|
|
placeholder="example.com"
|
|
maxLength={255}
|
|
className="input input-bordered w-full"
|
|
/>
|
|
<span className="text-xs text-base-content/50">
|
|
You can connect Search Console and set up rank tracking after
|
|
creating the project.
|
|
</span>
|
|
</label>
|
|
|
|
<div className="flex justify-end gap-2">
|
|
<button
|
|
type="button"
|
|
className="btn btn-ghost btn-sm"
|
|
onClick={onClose}
|
|
disabled={isPending}
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
type="submit"
|
|
className="btn btn-primary btn-sm"
|
|
disabled={isPending}
|
|
>
|
|
Create project
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</Modal>
|
|
);
|
|
}
|