Initial project creation panel
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { useState, type FormEvent } from "react";
|
||||
import { Check, ChevronRight, GitBranch, Loader2, Terminal } from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
|
||||
type CreationResult = {
|
||||
name: string;
|
||||
path: string;
|
||||
gitInitialized: boolean;
|
||||
};
|
||||
|
||||
export function ProjectCreator({ projectsRoot }: { projectsRoot: string }) {
|
||||
const [name, setName] = useState("");
|
||||
const [isCreating, setIsCreating] = useState(false);
|
||||
const [error, setError] = useState("");
|
||||
const [createdProject, setCreatedProject] = useState<CreationResult | null>(null);
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault();
|
||||
setIsCreating(true);
|
||||
setError("");
|
||||
setCreatedProject(null);
|
||||
|
||||
try {
|
||||
const response = await fetch("/api/projects", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ name }),
|
||||
});
|
||||
const data = await response.json();
|
||||
|
||||
if (!response.ok) {
|
||||
setError(data.error || "The project could not be created.");
|
||||
return;
|
||||
}
|
||||
|
||||
setCreatedProject(data);
|
||||
setName("");
|
||||
} catch {
|
||||
setError("Could not reach the local server. Try again.");
|
||||
} finally {
|
||||
setIsCreating(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section className="rounded-xl border border-stone-300 bg-[hsl(var(--card))] p-6 shadow-[0_24px_70px_-38px_rgba(28,25,23,0.45)] sm:p-8">
|
||||
<div className="mb-8 flex items-start justify-between gap-5">
|
||||
<div>
|
||||
<div className="mb-4 flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100 text-orange-700">
|
||||
<Terminal size={19} />
|
||||
</div>
|
||||
<h2 className="text-xl font-semibold tracking-[-0.025em] text-stone-950">
|
||||
New project
|
||||
</h2>
|
||||
<p className="mt-1.5 text-sm leading-6 text-stone-500">
|
||||
A directory, plus a Git repository.
|
||||
</p>
|
||||
</div>
|
||||
<span className="font-mono text-xs text-stone-400">01 / 01</span>
|
||||
</div>
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-6">
|
||||
<div className="space-y-2.5">
|
||||
<Label htmlFor="project-name">Project name</Label>
|
||||
<Input
|
||||
id="project-name"
|
||||
name="name"
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
placeholder="e.g. morning-star"
|
||||
autoComplete="off"
|
||||
autoFocus
|
||||
disabled={isCreating}
|
||||
aria-describedby="project-name-help"
|
||||
/>
|
||||
<p id="project-name-help" className="text-xs leading-5 text-stone-500">
|
||||
Letters, numbers, spaces, hyphens, and underscores.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="rounded-lg border border-dashed border-stone-300 bg-stone-50/70 px-4 py-3.5">
|
||||
<div className="mb-1 flex items-center gap-2 font-mono text-[10px] uppercase tracking-[0.14em] text-stone-400">
|
||||
<GitBranch size={12} />
|
||||
Destination
|
||||
</div>
|
||||
<p className="break-all font-mono text-xs leading-5 text-stone-600">
|
||||
{projectsRoot}/<span className="text-orange-700">{name.trim() || "project-name"}</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<p role="alert" className="rounded-md border border-red-200 bg-red-50 px-3.5 py-3 text-sm text-red-700">
|
||||
{error}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{createdProject ? (
|
||||
<div role="status" className="rounded-md border border-emerald-200 bg-emerald-50 px-3.5 py-3 text-sm text-emerald-800">
|
||||
<div className="flex items-center gap-2 font-medium">
|
||||
<Check size={16} />
|
||||
Project created and Git initialized.
|
||||
</div>
|
||||
<p className="mt-1 break-all font-mono text-xs text-emerald-700">
|
||||
{createdProject.path}
|
||||
</p>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<Button type="submit" size="lg" className="w-full" disabled={isCreating || !name.trim()}>
|
||||
{isCreating ? (
|
||||
<>
|
||||
<Loader2 className="mr-2 animate-spin" size={17} />
|
||||
Creating workspace
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
Create project
|
||||
<ChevronRight className="ml-2" size={17} />
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</form>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
import * as React from "react";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const buttonVariants = cva(
|
||||
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-primary text-primary-foreground shadow-sm hover:bg-primary/90",
|
||||
outline: "border border-input bg-background hover:bg-muted",
|
||||
},
|
||||
size: {
|
||||
default: "h-10 px-4 py-2",
|
||||
lg: "h-12 px-6 text-base",
|
||||
icon: "h-10 w-10",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
size: "default",
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export interface ButtonProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {}
|
||||
|
||||
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ className, variant, size, ...props }, ref) => (
|
||||
<button
|
||||
className={cn(buttonVariants({ variant, size, className }))}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Button.displayName = "Button";
|
||||
|
||||
export { Button, buttonVariants };
|
||||
@@ -0,0 +1,20 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const Input = React.forwardRef<HTMLInputElement, React.ComponentProps<"input">>(
|
||||
({ className, type, ...props }, ref) => (
|
||||
<input
|
||||
type={type}
|
||||
className={cn(
|
||||
"flex h-12 w-full rounded-md border border-input bg-background px-4 py-2 text-base shadow-sm outline-none transition placeholder:text-muted-foreground focus:border-ring focus:ring-2 focus:ring-ring/20 disabled:cursor-not-allowed disabled:opacity-50",
|
||||
className,
|
||||
)}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
),
|
||||
);
|
||||
Input.displayName = "Input";
|
||||
|
||||
export { Input };
|
||||
@@ -0,0 +1,14 @@
|
||||
import * as React from "react";
|
||||
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
function Label({ className, ...props }: React.ComponentProps<"label">) {
|
||||
return (
|
||||
<label
|
||||
className={cn("text-sm font-medium leading-none", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export { Label };
|
||||
Reference in New Issue
Block a user