Initial project creation panel
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
# Projects are created below this directory. Defaults to ~/projects.
|
||||||
|
PROJECTS_ROOT=/home/your-user/projects
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": ["next/core-web-vitals", "next/typescript"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
.env*
|
||||||
|
!.env.example
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# Forge
|
||||||
|
|
||||||
|
A small local panel for creating project directories with Git initialized.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Node.js 20+
|
||||||
|
- Git available on `PATH`
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
cp .env.example .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `PROJECTS_ROOT` in `.env.local` to the directory that should contain your projects. If it is omitted, Forge uses `~/projects`.
|
||||||
|
|
||||||
|
Start the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open `http://localhost:3000`.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
The API only accepts project names and never accepts an arbitrary filesystem path. It rejects names that could escape the configured root or collide with an existing directory.
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import { mkdir, rm } from "node:fs/promises";
|
||||||
|
import { execFile } from "node:child_process";
|
||||||
|
import { promisify } from "node:util";
|
||||||
|
|
||||||
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
import {
|
||||||
|
getProjectPath,
|
||||||
|
getProjectsRoot,
|
||||||
|
projectNameSchema,
|
||||||
|
} from "@/lib/projects";
|
||||||
|
|
||||||
|
export const runtime = "nodejs";
|
||||||
|
|
||||||
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
|
export async function POST(request: Request) {
|
||||||
|
let body: unknown;
|
||||||
|
|
||||||
|
try {
|
||||||
|
body = await request.json();
|
||||||
|
} catch {
|
||||||
|
return NextResponse.json({ error: "The request body must be valid JSON." }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = projectNameSchema.safeParse(
|
||||||
|
typeof body === "object" && body !== null && "name" in body
|
||||||
|
? (body as { name: unknown }).name
|
||||||
|
: undefined,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: result.error.issues[0]?.message || "Enter a valid project name." },
|
||||||
|
{ status: 400 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = result.data;
|
||||||
|
const projectPath = getProjectPath(name);
|
||||||
|
let directoryCreated = false;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await mkdir(getProjectsRoot(), { recursive: true });
|
||||||
|
await mkdir(projectPath);
|
||||||
|
directoryCreated = true;
|
||||||
|
|
||||||
|
await execFileAsync("git", ["init"], { cwd: projectPath });
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{ name, path: projectPath, gitInitialized: true },
|
||||||
|
{ status: 201 },
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if (directoryCreated) {
|
||||||
|
await rm(projectPath, { recursive: true, force: true }).catch(() => undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = error && typeof error === "object" && "code" in error
|
||||||
|
? (error as { code?: string }).code
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
if (code === "EEXIST") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "A project with that name already exists." },
|
||||||
|
{ status: 409 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code === "ENOENT") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Git was not found. Install Git and try again." },
|
||||||
|
{ status: 503 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("Project creation failed", error);
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "The project could not be created. Check the server logs for details." },
|
||||||
|
{ status: 500 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
@tailwind base;
|
||||||
|
@tailwind components;
|
||||||
|
@tailwind utilities;
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--font-geist-sans: Arial, Helvetica, sans-serif;
|
||||||
|
--font-geist-mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
|
||||||
|
--background: 36 33% 96%;
|
||||||
|
--foreground: 225 24% 14%;
|
||||||
|
--card: 40 33% 99%;
|
||||||
|
--card-foreground: 225 24% 14%;
|
||||||
|
--muted: 36 20% 91%;
|
||||||
|
--muted-foreground: 225 10% 43%;
|
||||||
|
--primary: 225 24% 14%;
|
||||||
|
--primary-foreground: 36 33% 96%;
|
||||||
|
--border: 35 18% 84%;
|
||||||
|
--input: 35 18% 78%;
|
||||||
|
--ring: 14 72% 53%;
|
||||||
|
--radius: 0.65rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
min-height: 100vh;
|
||||||
|
background: hsl(var(--background));
|
||||||
|
color: hsl(var(--foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: hsl(14 72% 53% / 0.22);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer utilities {
|
||||||
|
.text-balance {
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Forge | Project workspace",
|
||||||
|
description: "Create a local project workspace with Git initialized.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>{children}</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
import { FolderKanban } from "lucide-react";
|
||||||
|
|
||||||
|
import { ProjectCreator } from "@/components/project-creator";
|
||||||
|
import { getProjectsRoot } from "@/lib/projects";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
return (
|
||||||
|
<main className="relative min-h-screen overflow-hidden px-5 py-6 sm:px-8 lg:px-12">
|
||||||
|
<div className="pointer-events-none absolute -right-36 -top-40 h-[30rem] w-[30rem] rounded-full bg-orange-200/40 blur-3xl" />
|
||||||
|
<div className="pointer-events-none absolute -bottom-48 -left-36 h-[26rem] w-[26rem] rounded-full bg-stone-200/60 blur-3xl" />
|
||||||
|
|
||||||
|
<div className="relative mx-auto flex min-h-[calc(100vh-3rem)] max-w-7xl flex-col">
|
||||||
|
<header className="flex items-center justify-between border-b border-stone-300/70 pb-5">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="flex h-9 w-9 items-center justify-center rounded-md bg-stone-950 text-orange-200 shadow-sm">
|
||||||
|
<FolderKanban size={18} strokeWidth={2.2} />
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-sm font-semibold tracking-[0.18em] text-stone-950">
|
||||||
|
FORGE
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="font-mono text-[11px] uppercase tracking-[0.16em] text-stone-500">
|
||||||
|
Local workspace utility
|
||||||
|
</span>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid flex-1 items-center gap-14 py-14 lg:grid-cols-[1.05fr_0.95fr] lg:gap-24">
|
||||||
|
<section>
|
||||||
|
<p className="mb-6 font-mono text-xs font-medium uppercase tracking-[0.24em] text-orange-700">
|
||||||
|
Project / 001
|
||||||
|
</p>
|
||||||
|
<h1 className="max-w-xl text-balance text-5xl font-semibold leading-[0.98] tracking-[-0.055em] text-stone-950 sm:text-7xl">
|
||||||
|
Start with a clean slate.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-7 max-w-lg text-lg leading-8 text-stone-600">
|
||||||
|
Create a local project workspace and initialize its Git history in one deliberate step.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-12 flex items-center gap-3 text-sm text-stone-500">
|
||||||
|
<span className="h-2 w-2 rounded-full bg-emerald-500 shadow-[0_0_0_4px_rgba(16,185,129,0.12)]" />
|
||||||
|
Ready to create
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<ProjectCreator projectsRoot={getProjectsRoot()} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="flex flex-col gap-2 border-t border-stone-300/70 pt-5 font-mono text-[11px] uppercase tracking-[0.14em] text-stone-500 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<span>Git initialization enabled</span>
|
||||||
|
<span>v0.1 / Local only</span>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "default",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "tailwind.config.ts",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "stone",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 };
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const projectNameSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.min(1, "Enter a project name.")
|
||||||
|
.max(64, "Project names must be 64 characters or fewer.")
|
||||||
|
.regex(
|
||||||
|
/^[a-zA-Z0-9][a-zA-Z0-9 _-]*$/,
|
||||||
|
"Use letters, numbers, spaces, hyphens, or underscores.",
|
||||||
|
)
|
||||||
|
.refine((name) => !name.includes(".."), "Project names cannot contain '..'.");
|
||||||
|
|
||||||
|
export function getProjectsRoot() {
|
||||||
|
return path.resolve(process.env.PROJECTS_ROOT || path.join(os.homedir(), "projects"));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getProjectPath(name: string) {
|
||||||
|
const root = getProjectsRoot();
|
||||||
|
const projectPath = path.resolve(root, name);
|
||||||
|
|
||||||
|
if (projectPath === root || !projectPath.startsWith(`${root}${path.sep}`)) {
|
||||||
|
throw new Error("The project path must remain inside the projects directory.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return projectPath;
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
Vendored
+6
@@ -0,0 +1,6 @@
|
|||||||
|
/// <reference types="next" />
|
||||||
|
/// <reference types="next/image-types/global" />
|
||||||
|
/// <reference path="./.next/types/routes.d.ts" />
|
||||||
|
|
||||||
|
// NOTE: This file should not be edited
|
||||||
|
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
reactStrictMode: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
Generated
+6545
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"name": "project-creation-panel",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "next lint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"class-variance-authority": "^0.7.1",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^0.468.0",
|
||||||
|
"next": "^15.2.0",
|
||||||
|
"react": "^19.0.0",
|
||||||
|
"react-dom": "^19.0.0",
|
||||||
|
"tailwind-merge": "^2.6.0",
|
||||||
|
"zod": "^3.24.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"@types/react": "^19.0.3",
|
||||||
|
"@types/react-dom": "^19.0.2",
|
||||||
|
"autoprefixer": "^10.4.20",
|
||||||
|
"eslint": "^8.57.1",
|
||||||
|
"eslint-config-next": "^15.2.0",
|
||||||
|
"postcss": "^8.4.49",
|
||||||
|
"tailwindcss": "^3.4.17",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
/** @type {import('postcss-load-config').Config} */
|
||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
tailwindcss: {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
|
const config: Config = {
|
||||||
|
darkMode: ["class"],
|
||||||
|
content: [
|
||||||
|
"./app/**/*.{ts,tsx}",
|
||||||
|
"./components/**/*.{ts,tsx}",
|
||||||
|
"./lib/**/*.{ts,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
border: "hsl(var(--border))",
|
||||||
|
input: "hsl(var(--input))",
|
||||||
|
ring: "hsl(var(--ring))",
|
||||||
|
background: "hsl(var(--background))",
|
||||||
|
foreground: "hsl(var(--foreground))",
|
||||||
|
muted: {
|
||||||
|
DEFAULT: "hsl(var(--muted))",
|
||||||
|
foreground: "hsl(var(--muted-foreground))",
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
DEFAULT: "hsl(var(--primary))",
|
||||||
|
foreground: "hsl(var(--primary-foreground))",
|
||||||
|
},
|
||||||
|
card: {
|
||||||
|
DEFAULT: "hsl(var(--card))",
|
||||||
|
foreground: "hsl(var(--card-foreground))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
borderRadius: {
|
||||||
|
lg: "var(--radius)",
|
||||||
|
md: "calc(var(--radius) - 2px)",
|
||||||
|
sm: "calc(var(--radius) - 4px)",
|
||||||
|
},
|
||||||
|
fontFamily: {
|
||||||
|
sans: ["var(--font-geist-sans)"],
|
||||||
|
mono: ["var(--font-geist-mono)"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2017",
|
||||||
|
"lib": ["dom", "dom.iterable", "esnext"],
|
||||||
|
"allowJs": false,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"module": "esnext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"incremental": true,
|
||||||
|
"plugins": [{ "name": "next" }],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["next-env.d.ts", ".next/types/**/*.ts", "**/*.ts", "**/*.tsx"],
|
||||||
|
"exclude": ["node_modules"]
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user