forge-anvil-app 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/bootstrap/admin/[slug]/[id]/delete/page.tsx +55 -0
- package/bootstrap/admin/[slug]/[id]/edit/page.tsx +55 -0
- package/bootstrap/admin/[slug]/[id]/page.tsx +12 -0
- package/bootstrap/admin/[slug]/create/page.tsx +43 -0
- package/bootstrap/admin/[slug]/page.tsx +32 -0
- package/bootstrap/admin/layout.tsx +29 -0
- package/bootstrap/admin/page.tsx +38 -0
- package/bootstrap/components/anvil/AnvilForm.tsx +182 -0
- package/bootstrap/components/anvil/AnvilTable.tsx +113 -0
- package/bootstrap/components/anvil/CreateResourceForm.tsx +47 -0
- package/bootstrap/components/anvil/DeleteResourceForm.tsx +96 -0
- package/bootstrap/components/anvil/EditResourceForm.tsx +51 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +137 -0
- package/dist/index.js.map +1 -0
- package/package.json +27 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# create-anvil-app
|
|
2
|
+
|
|
3
|
+
Create a new Next-anvil application with a single command.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx create-anvil-app my-app
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This will:
|
|
12
|
+
1. Create a new Next.js app with TypeScript and Tailwind CSS
|
|
13
|
+
2. Install `next-anvil` package
|
|
14
|
+
3. Set up Anvil-specific configuration
|
|
15
|
+
4. Create example Prisma schema
|
|
16
|
+
5. Create an admin dashboard page
|
|
17
|
+
|
|
18
|
+
## What Gets Created
|
|
19
|
+
|
|
20
|
+
- Next.js app with TypeScript and Tailwind CSS
|
|
21
|
+
- `next-anvil` package installed
|
|
22
|
+
- `lib/resources/` directory for your resources
|
|
23
|
+
- Example Prisma schema
|
|
24
|
+
- Example admin page at `/admin`
|
|
25
|
+
- `.env.local` with database configuration
|
|
26
|
+
|
|
27
|
+
## Next Steps
|
|
28
|
+
|
|
29
|
+
After running `create-anvil-app`:
|
|
30
|
+
|
|
31
|
+
1. `cd my-app`
|
|
32
|
+
2. Update `.env.local` with your database URL
|
|
33
|
+
3. Run `anvil db:migrate` to set up the database
|
|
34
|
+
4. Run `npm run dev` to start the development server
|
|
35
|
+
5. Visit `http://localhost:3000/admin`
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin page for deleting resources
|
|
3
|
+
* Server Component - handles deletion with confirmation
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { deleteRecord, getRecord } from "@/lib/anvil/actions";
|
|
7
|
+
import { AnvilResource } from "@/lib/anvil/resource";
|
|
8
|
+
import { redirect } from "next/navigation";
|
|
9
|
+
import { notFound } from "next/navigation";
|
|
10
|
+
import { DeleteResourceForm } from "@/components/admin/DeleteResourceForm";
|
|
11
|
+
|
|
12
|
+
export default async function DeleteResourcePage({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: Promise<{ slug: string; id: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const { slug, id } = await params;
|
|
18
|
+
const resource = (await import(`@/lib/resources/${slug}`))
|
|
19
|
+
.default as AnvilResource;
|
|
20
|
+
|
|
21
|
+
// Fetch the existing record
|
|
22
|
+
const record = await getRecord(resource, id);
|
|
23
|
+
if (!record) {
|
|
24
|
+
notFound();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function handleDelete() {
|
|
28
|
+
"use server";
|
|
29
|
+
const result = await deleteRecord(resource, id);
|
|
30
|
+
if (result.success) {
|
|
31
|
+
redirect(`/admin/${resource.slug}`);
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(result.errors?.[0]?.message || "Failed to delete record");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-6">
|
|
39
|
+
<div>
|
|
40
|
+
<h1 className="text-3xl font-bold text-gray-900">
|
|
41
|
+
Delete {resource.label.slice(0, -1)}
|
|
42
|
+
</h1>
|
|
43
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
44
|
+
This action cannot be undone. Please confirm that you want to delete
|
|
45
|
+
this {resource.model.toLowerCase()}.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
<DeleteResourceForm
|
|
49
|
+
record={record}
|
|
50
|
+
resourceLabel={resource.model.toLowerCase()}
|
|
51
|
+
onDelete={handleDelete}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin page for editing resources
|
|
3
|
+
* Server Component - handles data fetching and server actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { EditResourceForm } from "@/components/admin/EditResourceForm";
|
|
7
|
+
import { updateRecord, getRecord } from "@/lib/anvil/actions";
|
|
8
|
+
import { AnvilResource } from "@/lib/anvil/resource";
|
|
9
|
+
import { redirect } from "next/navigation";
|
|
10
|
+
import { notFound } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
export default async function EditResourcePage({
|
|
13
|
+
params,
|
|
14
|
+
}: {
|
|
15
|
+
params: Promise<{ slug: string; id: string }>;
|
|
16
|
+
}) {
|
|
17
|
+
const { slug, id } = await params;
|
|
18
|
+
const resource = (await import(`@/lib/resources/${slug}`))
|
|
19
|
+
.default as AnvilResource;
|
|
20
|
+
|
|
21
|
+
// Fetch the existing record
|
|
22
|
+
const record = await getRecord(resource, id);
|
|
23
|
+
if (!record) {
|
|
24
|
+
notFound();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function handleUpdate(data: Record<string, any>) {
|
|
28
|
+
"use server";
|
|
29
|
+
const result = await updateRecord(resource, id, data);
|
|
30
|
+
if (result.success) {
|
|
31
|
+
redirect(`/admin/${resource.slug}`);
|
|
32
|
+
} else {
|
|
33
|
+
throw new Error(result.errors?.[0]?.message || "Failed to update record");
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<div className="space-y-6">
|
|
39
|
+
<div>
|
|
40
|
+
<h1 className="text-3xl font-bold text-gray-900">
|
|
41
|
+
Edit {resource.model.toLowerCase()}
|
|
42
|
+
</h1>
|
|
43
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
44
|
+
Update the information below to modify this{" "}
|
|
45
|
+
{resource.model.toLowerCase()}.
|
|
46
|
+
</p>
|
|
47
|
+
</div>
|
|
48
|
+
<EditResourceForm
|
|
49
|
+
schema={resource.editForm}
|
|
50
|
+
initialData={record}
|
|
51
|
+
onSubmit={handleUpdate}
|
|
52
|
+
/>
|
|
53
|
+
</div>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AnvilResource } from "@/lib/anvil/resource";
|
|
2
|
+
|
|
3
|
+
export default async function ResourcePage({
|
|
4
|
+
params,
|
|
5
|
+
}: {
|
|
6
|
+
params: Promise<{ slug: string; id: string }>;
|
|
7
|
+
}) {
|
|
8
|
+
const { slug, id } = await params;
|
|
9
|
+
const resource = (await import(`@/lib/resources/${slug}`))
|
|
10
|
+
.default as AnvilResource;
|
|
11
|
+
return <div>ResourcePage</div>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin page for creating resources
|
|
3
|
+
* Server Component - handles data fetching and server actions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { CreateResourceForm } from "@/components/admin/CreateResourceForm";
|
|
7
|
+
import { createRecord } from "@/lib/anvil/actions";
|
|
8
|
+
import { redirect } from "next/navigation";
|
|
9
|
+
import { AnvilResource } from "@/lib/anvil/resource";
|
|
10
|
+
|
|
11
|
+
export default async function CreateResourcePage({
|
|
12
|
+
params,
|
|
13
|
+
}: {
|
|
14
|
+
params: Promise<{ slug: string }>;
|
|
15
|
+
}) {
|
|
16
|
+
const { slug } = await params;
|
|
17
|
+
const resource = (await import(`@/lib/resources/${slug}`))
|
|
18
|
+
.default as AnvilResource;
|
|
19
|
+
|
|
20
|
+
async function handleCreate(data: Record<string, any>) {
|
|
21
|
+
"use server";
|
|
22
|
+
const result = await createRecord(resource, data);
|
|
23
|
+
if (result.success) {
|
|
24
|
+
redirect(`/admin/${resource.slug}`);
|
|
25
|
+
} else {
|
|
26
|
+
throw new Error(result.errors?.[0]?.message || "Failed to create record");
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="space-y-6">
|
|
32
|
+
<div>
|
|
33
|
+
<h1 className="text-3xl font-bold text-gray-900">
|
|
34
|
+
Create {resource.label.slice(0, -1)}
|
|
35
|
+
</h1>
|
|
36
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
37
|
+
Fill in the form below to create a new {resource.label.toLowerCase()}.
|
|
38
|
+
</p>
|
|
39
|
+
</div>
|
|
40
|
+
<CreateResourceForm schema={resource.form} onSubmit={handleCreate} />
|
|
41
|
+
</div>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin page for Users resource
|
|
3
|
+
* Generic renderer that reads from resource.ts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import AnvilTable from "@/components/admin/AnvilTable";
|
|
7
|
+
import Link from "next/link";
|
|
8
|
+
import { AnvilResource } from "@/lib/anvil/resource";
|
|
9
|
+
|
|
10
|
+
export default async function ResourceAdminPage({
|
|
11
|
+
params,
|
|
12
|
+
}: {
|
|
13
|
+
params: Promise<{ slug: string }>;
|
|
14
|
+
}) {
|
|
15
|
+
const { slug } = await params;
|
|
16
|
+
const resource = (await import(`@/lib/resources/${slug}`))
|
|
17
|
+
.default as AnvilResource;
|
|
18
|
+
return (
|
|
19
|
+
<div className="space-y-6">
|
|
20
|
+
<div className="flex justify-between items-center">
|
|
21
|
+
<h1 className="text-3xl font-bold text-gray-900">{resource.label}</h1>
|
|
22
|
+
<Link
|
|
23
|
+
href={`/admin/${resource.slug}/create`}
|
|
24
|
+
className="inline-flex items-center px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 transition-colors"
|
|
25
|
+
>
|
|
26
|
+
Create {resource.label.slice(0, -1)}
|
|
27
|
+
</Link>
|
|
28
|
+
</div>
|
|
29
|
+
<AnvilTable resource={resource} />
|
|
30
|
+
</div>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function AdminLayout({
|
|
4
|
+
children,
|
|
5
|
+
}: {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
}) {
|
|
8
|
+
return (
|
|
9
|
+
<div className="min-h-screen bg-gray-50">
|
|
10
|
+
<nav className="bg-white border-b border-gray-200">
|
|
11
|
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
12
|
+
<div className="flex justify-between h-16">
|
|
13
|
+
<div className="flex">
|
|
14
|
+
<Link
|
|
15
|
+
href="/admin"
|
|
16
|
+
className="flex items-center px-4 text-lg font-semibold text-gray-900"
|
|
17
|
+
>
|
|
18
|
+
Admin
|
|
19
|
+
</Link>
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
</nav>
|
|
24
|
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
|
|
25
|
+
{children}
|
|
26
|
+
</main>
|
|
27
|
+
</div>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin dashboard/index page
|
|
3
|
+
* Lists all available resources
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import Link from "next/link";
|
|
7
|
+
|
|
8
|
+
// TODO: Auto-discover resources from /app/admin/*/resource.ts
|
|
9
|
+
const resources = [{ slug: "users", label: "Users" }];
|
|
10
|
+
|
|
11
|
+
export default function AdminDashboard() {
|
|
12
|
+
return (
|
|
13
|
+
<div className="space-y-6">
|
|
14
|
+
<div>
|
|
15
|
+
<h1 className="text-3xl font-bold text-gray-900">Admin Dashboard</h1>
|
|
16
|
+
<p className="mt-2 text-sm text-gray-600">
|
|
17
|
+
Manage your application resources
|
|
18
|
+
</p>
|
|
19
|
+
</div>
|
|
20
|
+
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
|
21
|
+
{resources.map((resource) => (
|
|
22
|
+
<Link
|
|
23
|
+
key={resource.slug}
|
|
24
|
+
href={`/admin/${resource.slug}`}
|
|
25
|
+
className="block p-6 bg-white rounded-lg shadow hover:shadow-md transition-all border border-gray-200 hover:border-blue-300"
|
|
26
|
+
>
|
|
27
|
+
<h2 className="text-lg font-semibold text-gray-900">
|
|
28
|
+
{resource.label}
|
|
29
|
+
</h2>
|
|
30
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
31
|
+
Manage {resource.label.toLowerCase()}
|
|
32
|
+
</p>
|
|
33
|
+
</Link>
|
|
34
|
+
))}
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import type { FieldConfig } from "next-anvil/fields";
|
|
5
|
+
import { FormSchema } from "next-anvil/form";
|
|
6
|
+
import {
|
|
7
|
+
TextField,
|
|
8
|
+
EmailField,
|
|
9
|
+
SelectField,
|
|
10
|
+
DateField,
|
|
11
|
+
NumberField,
|
|
12
|
+
TextareaField,
|
|
13
|
+
} from "next-anvil/components/fields";
|
|
14
|
+
|
|
15
|
+
interface AnvilFormProps {
|
|
16
|
+
schema: FormSchema;
|
|
17
|
+
initialData?: Record<string, any>;
|
|
18
|
+
onSubmit: (data: Record<string, any>) => Promise<void>;
|
|
19
|
+
onCancel?: () => void;
|
|
20
|
+
submitting?: boolean;
|
|
21
|
+
errors?: Record<string, string>;
|
|
22
|
+
submitLabel?: string;
|
|
23
|
+
cancelLabel?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function AnvilForm({
|
|
27
|
+
schema,
|
|
28
|
+
initialData = {},
|
|
29
|
+
onSubmit,
|
|
30
|
+
onCancel,
|
|
31
|
+
submitting: externalSubmitting,
|
|
32
|
+
errors: externalErrors = {},
|
|
33
|
+
submitLabel = "Save",
|
|
34
|
+
cancelLabel = "Cancel",
|
|
35
|
+
}: AnvilFormProps) {
|
|
36
|
+
const [formData, setFormData] = useState<Record<string, any>>(initialData);
|
|
37
|
+
const [internalSubmitting, setInternalSubmitting] = useState(false);
|
|
38
|
+
const [internalErrors, setInternalErrors] = useState<Record<string, string>>(
|
|
39
|
+
{}
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const submitting = externalSubmitting ?? internalSubmitting;
|
|
43
|
+
const errors = { ...internalErrors, ...externalErrors };
|
|
44
|
+
|
|
45
|
+
async function handleSubmit(e: React.FormEvent) {
|
|
46
|
+
e.preventDefault();
|
|
47
|
+
setInternalSubmitting(true);
|
|
48
|
+
setInternalErrors({});
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
await onSubmit(formData);
|
|
52
|
+
} catch (error: any) {
|
|
53
|
+
const errorMap: Record<string, string> = {};
|
|
54
|
+
if (error.field) {
|
|
55
|
+
errorMap[error.field] = error.message;
|
|
56
|
+
} else {
|
|
57
|
+
errorMap._general = error.message || "An error occurred";
|
|
58
|
+
}
|
|
59
|
+
setInternalErrors(errorMap);
|
|
60
|
+
} finally {
|
|
61
|
+
setInternalSubmitting(false);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function renderField(
|
|
66
|
+
fieldName: string,
|
|
67
|
+
fieldConfig: FieldConfig,
|
|
68
|
+
value: any
|
|
69
|
+
) {
|
|
70
|
+
// Skip hidden fields in form rendering
|
|
71
|
+
if (fieldConfig.type === "hidden") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const fieldError = errors[fieldName];
|
|
76
|
+
const isDisabled = submitting || fieldConfig.readOnly;
|
|
77
|
+
|
|
78
|
+
const commonProps = {
|
|
79
|
+
fieldName,
|
|
80
|
+
fieldConfig: fieldConfig as any,
|
|
81
|
+
value,
|
|
82
|
+
error: fieldError,
|
|
83
|
+
disabled: isDisabled,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
switch (fieldConfig.type) {
|
|
87
|
+
case "text":
|
|
88
|
+
return (
|
|
89
|
+
<TextField
|
|
90
|
+
key={fieldName}
|
|
91
|
+
{...commonProps}
|
|
92
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
93
|
+
/>
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
case "email":
|
|
97
|
+
return (
|
|
98
|
+
<EmailField
|
|
99
|
+
key={fieldName}
|
|
100
|
+
{...commonProps}
|
|
101
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
102
|
+
/>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
case "select":
|
|
106
|
+
return (
|
|
107
|
+
<SelectField
|
|
108
|
+
key={fieldName}
|
|
109
|
+
{...commonProps}
|
|
110
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
111
|
+
/>
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
case "date":
|
|
115
|
+
return (
|
|
116
|
+
<DateField
|
|
117
|
+
key={fieldName}
|
|
118
|
+
{...commonProps}
|
|
119
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
120
|
+
/>
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
case "number":
|
|
124
|
+
return (
|
|
125
|
+
<NumberField
|
|
126
|
+
key={fieldName}
|
|
127
|
+
{...commonProps}
|
|
128
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
case "textarea":
|
|
133
|
+
return (
|
|
134
|
+
<TextareaField
|
|
135
|
+
key={fieldName}
|
|
136
|
+
{...commonProps}
|
|
137
|
+
onChange={(val) => setFormData({ ...formData, [fieldName]: val })}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
|
|
141
|
+
default:
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return (
|
|
147
|
+
<form onSubmit={handleSubmit} className="bg-white shadow rounded-lg p-6">
|
|
148
|
+
{errors._general && (
|
|
149
|
+
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
|
|
150
|
+
<p className="font-medium">Error</p>
|
|
151
|
+
<p className="text-sm mt-1">{errors._general}</p>
|
|
152
|
+
</div>
|
|
153
|
+
)}
|
|
154
|
+
|
|
155
|
+
<div className="grid grid-cols-4 gap-6">
|
|
156
|
+
{Object.entries(schema.fields || {}).map(([fieldName, fieldConfig]) =>
|
|
157
|
+
renderField(fieldName, fieldConfig, formData[fieldName])
|
|
158
|
+
)}
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
<div className="mt-8 flex gap-3 border-t border-gray-200 pt-6">
|
|
162
|
+
<button
|
|
163
|
+
type="submit"
|
|
164
|
+
disabled={submitting}
|
|
165
|
+
className="px-4 py-2 bg-blue-600 text-white text-sm font-medium rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
166
|
+
>
|
|
167
|
+
{submitting ? "Saving..." : submitLabel}
|
|
168
|
+
</button>
|
|
169
|
+
{onCancel && (
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
onClick={onCancel}
|
|
173
|
+
disabled={submitting}
|
|
174
|
+
className="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-300 disabled:opacity-50 transition-colors"
|
|
175
|
+
>
|
|
176
|
+
{cancelLabel}
|
|
177
|
+
</button>
|
|
178
|
+
)}
|
|
179
|
+
</div>
|
|
180
|
+
</form>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { AnvilResource } from "next-anvil/resource";
|
|
2
|
+
import { TableColumn } from "next-anvil/table";
|
|
3
|
+
import { prisma } from "@/lib/prisma";
|
|
4
|
+
import Link from "next/link";
|
|
5
|
+
|
|
6
|
+
export default async function AnvilTable({
|
|
7
|
+
resource,
|
|
8
|
+
}: {
|
|
9
|
+
resource: AnvilResource;
|
|
10
|
+
}) {
|
|
11
|
+
const records = await (prisma as any)[resource.model].findMany();
|
|
12
|
+
|
|
13
|
+
if (records.length === 0) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="bg-white shadow rounded-lg p-12 text-center">
|
|
16
|
+
<p className="text-gray-500 text-lg">No records found</p>
|
|
17
|
+
<p className="text-gray-400 text-sm mt-2">
|
|
18
|
+
Get started by creating a new {resource.label.toLowerCase()}
|
|
19
|
+
</p>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<div className="bg-white shadow rounded-lg overflow-hidden">
|
|
26
|
+
<div className="overflow-x-auto">
|
|
27
|
+
<table className="min-w-full divide-y divide-gray-200">
|
|
28
|
+
<thead className="bg-gray-50">
|
|
29
|
+
<tr>
|
|
30
|
+
{resource.table.columns.map((col: TableColumn) => {
|
|
31
|
+
if (col.visible === "never") return null;
|
|
32
|
+
return (
|
|
33
|
+
<th
|
|
34
|
+
key={col.name}
|
|
35
|
+
className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
|
36
|
+
>
|
|
37
|
+
{col.label || col.name}
|
|
38
|
+
</th>
|
|
39
|
+
);
|
|
40
|
+
})}
|
|
41
|
+
<th className="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
|
42
|
+
Actions
|
|
43
|
+
</th>
|
|
44
|
+
</tr>
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody className="bg-white divide-y divide-gray-200">
|
|
47
|
+
{records.map((record: any) => (
|
|
48
|
+
<tr
|
|
49
|
+
key={record.id}
|
|
50
|
+
className="hover:bg-gray-50 transition-colors"
|
|
51
|
+
>
|
|
52
|
+
{resource.table.columns.map((col: TableColumn) => {
|
|
53
|
+
if (col.visible === "never") return null;
|
|
54
|
+
const value = record[col.name];
|
|
55
|
+
let displayValue = value;
|
|
56
|
+
|
|
57
|
+
if (value === null || value === undefined) {
|
|
58
|
+
displayValue = (
|
|
59
|
+
<span className="text-gray-400 italic">-</span>
|
|
60
|
+
);
|
|
61
|
+
} else if (value instanceof Date) {
|
|
62
|
+
displayValue = new Date(value).toLocaleDateString("en-US", {
|
|
63
|
+
year: "numeric",
|
|
64
|
+
month: "short",
|
|
65
|
+
day: "numeric",
|
|
66
|
+
});
|
|
67
|
+
} else if (typeof value === "boolean") {
|
|
68
|
+
displayValue = (
|
|
69
|
+
<span
|
|
70
|
+
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
|
71
|
+
value
|
|
72
|
+
? "bg-green-100 text-green-800"
|
|
73
|
+
: "bg-gray-100 text-gray-800"
|
|
74
|
+
}`}
|
|
75
|
+
>
|
|
76
|
+
{value ? "Yes" : "No"}
|
|
77
|
+
</span>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<td
|
|
83
|
+
key={col.name}
|
|
84
|
+
className="px-6 py-4 whitespace-nowrap text-sm text-gray-900"
|
|
85
|
+
>
|
|
86
|
+
{displayValue}
|
|
87
|
+
</td>
|
|
88
|
+
);
|
|
89
|
+
})}
|
|
90
|
+
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
|
91
|
+
<div className="flex justify-end gap-3">
|
|
92
|
+
<Link
|
|
93
|
+
href={`/admin/${resource.slug}/${record.id}/edit`}
|
|
94
|
+
className="text-blue-600 hover:text-blue-900 font-medium"
|
|
95
|
+
>
|
|
96
|
+
Edit
|
|
97
|
+
</Link>
|
|
98
|
+
<Link
|
|
99
|
+
href={`/admin/${resource.slug}/${record.id}/delete`}
|
|
100
|
+
className="text-red-600 hover:text-red-900 font-medium"
|
|
101
|
+
>
|
|
102
|
+
Delete
|
|
103
|
+
</Link>
|
|
104
|
+
</div>
|
|
105
|
+
</td>
|
|
106
|
+
</tr>
|
|
107
|
+
))}
|
|
108
|
+
</tbody>
|
|
109
|
+
</table>
|
|
110
|
+
</div>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CreateResourceForm - Client component wrapper for creating resources
|
|
3
|
+
* Separates client-side form logic from server-side data operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { FormSchema } from "next-anvil/form";
|
|
9
|
+
import { AnvilForm } from "./AnvilForm";
|
|
10
|
+
import { useRouter } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
interface CreateResourceFormProps {
|
|
13
|
+
schema: FormSchema;
|
|
14
|
+
onSubmit: (data: Record<string, any>) => Promise<void>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function CreateResourceForm({
|
|
18
|
+
schema,
|
|
19
|
+
onSubmit,
|
|
20
|
+
}: CreateResourceFormProps) {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
|
|
23
|
+
async function handleSubmit(data: Record<string, any>) {
|
|
24
|
+
try {
|
|
25
|
+
await onSubmit(data);
|
|
26
|
+
// Server action handles redirect, but refresh in case of client-side navigation
|
|
27
|
+
router.refresh();
|
|
28
|
+
} catch (error: any) {
|
|
29
|
+
// Error handling is done in the server action
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handleCancel() {
|
|
35
|
+
router.back();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<AnvilForm
|
|
40
|
+
schema={schema}
|
|
41
|
+
onSubmit={handleSubmit}
|
|
42
|
+
onCancel={handleCancel}
|
|
43
|
+
submitLabel="Create"
|
|
44
|
+
cancelLabel="Cancel"
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeleteResourceForm - Client component for confirming deletion
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
"use client";
|
|
6
|
+
|
|
7
|
+
import { useRouter } from "next/navigation";
|
|
8
|
+
import { useState } from "react";
|
|
9
|
+
|
|
10
|
+
interface DeleteResourceFormProps {
|
|
11
|
+
record: Record<string, any>;
|
|
12
|
+
resourceLabel: string;
|
|
13
|
+
onDelete: () => Promise<void>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function DeleteResourceForm({
|
|
17
|
+
record,
|
|
18
|
+
resourceLabel,
|
|
19
|
+
onDelete,
|
|
20
|
+
}: DeleteResourceFormProps) {
|
|
21
|
+
const router = useRouter();
|
|
22
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
23
|
+
const [error, setError] = useState<string | null>(null);
|
|
24
|
+
|
|
25
|
+
async function handleDelete() {
|
|
26
|
+
setIsDeleting(true);
|
|
27
|
+
setError(null);
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
await onDelete();
|
|
31
|
+
router.push(`/admin/`);
|
|
32
|
+
router.refresh();
|
|
33
|
+
} catch (error: any) {
|
|
34
|
+
setError(error.message || "Failed to delete record");
|
|
35
|
+
setIsDeleting(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function handleCancel() {
|
|
40
|
+
router.back();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Get a display value for the record (prefer name, email, or id)
|
|
44
|
+
const displayValue =
|
|
45
|
+
record.name ||
|
|
46
|
+
record.email ||
|
|
47
|
+
record.title ||
|
|
48
|
+
(record.firstName && record.lastName
|
|
49
|
+
? `${record.firstName} ${record.lastName}`
|
|
50
|
+
: record.firstName || record.lastName) ||
|
|
51
|
+
`#${record.id}`;
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<div className="bg-white shadow rounded-lg p-6">
|
|
55
|
+
{error && (
|
|
56
|
+
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-md text-red-700">
|
|
57
|
+
<p className="font-medium">Error</p>
|
|
58
|
+
<p className="text-sm mt-1">{error}</p>
|
|
59
|
+
</div>
|
|
60
|
+
)}
|
|
61
|
+
|
|
62
|
+
<div className="mb-6">
|
|
63
|
+
<p className="text-gray-700 mb-4">
|
|
64
|
+
Are you sure you want to delete this {resourceLabel.toLowerCase()}?
|
|
65
|
+
This action cannot be undone.
|
|
66
|
+
</p>
|
|
67
|
+
<div className="bg-gray-50 border border-gray-200 p-4 rounded-md">
|
|
68
|
+
<p className="font-medium text-gray-900">{displayValue}</p>
|
|
69
|
+
{record.email && (
|
|
70
|
+
<p className="text-sm text-gray-600 mt-1">{record.email}</p>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
74
|
+
|
|
75
|
+
<div className="flex gap-3 border-t border-gray-200 pt-6">
|
|
76
|
+
<button
|
|
77
|
+
type="button"
|
|
78
|
+
onClick={handleDelete}
|
|
79
|
+
disabled={isDeleting}
|
|
80
|
+
className="px-4 py-2 bg-red-600 text-white text-sm font-medium rounded-md hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
81
|
+
>
|
|
82
|
+
{isDeleting ? "Deleting..." : "Delete"}
|
|
83
|
+
</button>
|
|
84
|
+
<button
|
|
85
|
+
type="button"
|
|
86
|
+
onClick={handleCancel}
|
|
87
|
+
disabled={isDeleting}
|
|
88
|
+
className="px-4 py-2 bg-gray-200 text-gray-700 text-sm font-medium rounded-md hover:bg-gray-300 disabled:opacity-50 transition-colors"
|
|
89
|
+
>
|
|
90
|
+
Cancel
|
|
91
|
+
</button>
|
|
92
|
+
</div>
|
|
93
|
+
</div>
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EditResourceForm - Client component wrapper for editing resources
|
|
3
|
+
* Separates client-side form logic from server-side data operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use client";
|
|
7
|
+
|
|
8
|
+
import { FormSchema } from "next-anvil/form";
|
|
9
|
+
import { AnvilForm } from "./AnvilForm";
|
|
10
|
+
import { useRouter } from "next/navigation";
|
|
11
|
+
|
|
12
|
+
interface EditResourceFormProps {
|
|
13
|
+
schema: FormSchema;
|
|
14
|
+
initialData: Record<string, any>;
|
|
15
|
+
onSubmit: (data: Record<string, any>) => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function EditResourceForm({
|
|
19
|
+
schema,
|
|
20
|
+
initialData,
|
|
21
|
+
onSubmit,
|
|
22
|
+
}: EditResourceFormProps) {
|
|
23
|
+
const router = useRouter();
|
|
24
|
+
|
|
25
|
+
async function handleSubmit(data: Record<string, any>) {
|
|
26
|
+
try {
|
|
27
|
+
await onSubmit(data);
|
|
28
|
+
router.push(`/admin/`);
|
|
29
|
+
router.refresh();
|
|
30
|
+
} catch (error: any) {
|
|
31
|
+
// Error handling is done in the server action
|
|
32
|
+
throw error;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function handleCancel() {
|
|
37
|
+
router.back();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<AnvilForm
|
|
42
|
+
schema={schema}
|
|
43
|
+
initialData={initialData}
|
|
44
|
+
onSubmit={handleSubmit}
|
|
45
|
+
onCancel={handleCancel}
|
|
46
|
+
submitLabel="Update"
|
|
47
|
+
cancelLabel="Cancel"
|
|
48
|
+
/>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":""}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync, readFileSync, cpSync } from "fs";
|
|
4
|
+
import { join, dirname } from "path";
|
|
5
|
+
import { fileURLToPath } from "url";
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const projectName = process.argv[2];
|
|
9
|
+
if (!projectName) {
|
|
10
|
+
console.error("Please provide a project name:");
|
|
11
|
+
console.error(" npx create-anvil-app <project-name>");
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
const projectPath = join(process.cwd(), projectName);
|
|
15
|
+
if (existsSync(projectPath)) {
|
|
16
|
+
console.error(`Directory ${projectName} already exists.`);
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
console.log(`Creating Next-anvil app: ${projectName}\n`);
|
|
20
|
+
// Step 1: Create Next.js app
|
|
21
|
+
console.log("š¦ Creating Next.js app...");
|
|
22
|
+
try {
|
|
23
|
+
execSync(`npx create-next-app@latest ${projectName} --typescript --tailwind --app --import-alias "@/*" --use-npm --yes`, { stdio: "inherit", cwd: process.cwd() });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
console.error("Failed to create Next.js app:", error);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
console.log("\nā
Next.js app created successfully!");
|
|
30
|
+
// Step 2: Install next-anvil
|
|
31
|
+
console.log("\nš¦ Installing next-anvil...");
|
|
32
|
+
try {
|
|
33
|
+
execSync("npm install next-anvil", {
|
|
34
|
+
stdio: "inherit",
|
|
35
|
+
cwd: projectPath,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
console.error("Failed to install next-anvil:", error);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
// Step 2.5: Copy bootstrap files
|
|
43
|
+
console.log("\nš Copying Anvil bootstrap files...");
|
|
44
|
+
// When installed, __dirname points to dist/, so go up one level to find bootstrap
|
|
45
|
+
const bootstrapDir = join(__dirname, "..", "bootstrap");
|
|
46
|
+
// Copy admin folder to app/admin
|
|
47
|
+
const bootstrapAdminDir = join(bootstrapDir, "admin");
|
|
48
|
+
const targetAdminDir = join(projectPath, "app", "admin");
|
|
49
|
+
if (existsSync(bootstrapAdminDir)) {
|
|
50
|
+
cpSync(bootstrapAdminDir, targetAdminDir, { recursive: true });
|
|
51
|
+
console.log(" ā
Copied admin folder to app/admin");
|
|
52
|
+
}
|
|
53
|
+
// Copy components folder to src/components
|
|
54
|
+
const bootstrapComponentsDir = join(bootstrapDir, "components");
|
|
55
|
+
const targetComponentsDir = join(projectPath, "src", "components");
|
|
56
|
+
if (existsSync(bootstrapComponentsDir)) {
|
|
57
|
+
mkdirSync(join(projectPath, "src"), { recursive: true });
|
|
58
|
+
cpSync(bootstrapComponentsDir, targetComponentsDir, { recursive: true });
|
|
59
|
+
console.log(" ā
Copied components folder to src/components");
|
|
60
|
+
}
|
|
61
|
+
// Step 3: Add Anvil-specific files and configuration
|
|
62
|
+
console.log("\nšØ Setting up Anvil configuration...");
|
|
63
|
+
// Create lib/resources directory
|
|
64
|
+
const resourcesDir = join(projectPath, "anvil", "resources");
|
|
65
|
+
mkdirSync(resourcesDir, { recursive: true });
|
|
66
|
+
// Create .gitkeep to ensure directory exists
|
|
67
|
+
writeFileSync(join(resourcesDir, ".gitkeep"), "");
|
|
68
|
+
// Update package.json to add anvil script
|
|
69
|
+
const packageJsonPath = join(projectPath, "package.json");
|
|
70
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
71
|
+
if (!packageJson.scripts) {
|
|
72
|
+
packageJson.scripts = {};
|
|
73
|
+
}
|
|
74
|
+
packageJson.scripts.anvil = "anvil";
|
|
75
|
+
writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
|
|
76
|
+
// Create app layout with Anvil styles import
|
|
77
|
+
const appLayoutPath = join(projectPath, "app", "layout.tsx");
|
|
78
|
+
if (existsSync(appLayoutPath)) {
|
|
79
|
+
let layoutContent = readFileSync(appLayoutPath, "utf-8");
|
|
80
|
+
// Add Anvil styles import if not already present
|
|
81
|
+
if (!layoutContent.includes("next-anvil/styles.css")) {
|
|
82
|
+
// Find the last import statement
|
|
83
|
+
const lastImportIndex = layoutContent.lastIndexOf("import");
|
|
84
|
+
const nextLineAfterLastImport = layoutContent.indexOf("\n", lastImportIndex) + 1;
|
|
85
|
+
layoutContent =
|
|
86
|
+
layoutContent.slice(0, nextLineAfterLastImport) +
|
|
87
|
+
'import "next-anvil/styles.css";\n' +
|
|
88
|
+
layoutContent.slice(nextLineAfterLastImport);
|
|
89
|
+
writeFileSync(appLayoutPath, layoutContent);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// Create example .env.local with Prisma DATABASE_URL
|
|
93
|
+
const envLocalPath = join(projectPath, ".env.local");
|
|
94
|
+
if (!existsSync(envLocalPath)) {
|
|
95
|
+
writeFileSync(envLocalPath, `# Database
|
|
96
|
+
DATABASE_URL="postgresql://user:password@localhost:5432/anvil?schema=public"
|
|
97
|
+
|
|
98
|
+
# Next.js
|
|
99
|
+
NEXT_PUBLIC_APP_URL=http://localhost:3000
|
|
100
|
+
`);
|
|
101
|
+
}
|
|
102
|
+
// Create example prisma schema
|
|
103
|
+
const prismaDir = join(projectPath, "prisma");
|
|
104
|
+
mkdirSync(prismaDir, { recursive: true });
|
|
105
|
+
const prismaSchemaPath = join(prismaDir, "schema.prisma");
|
|
106
|
+
if (!existsSync(prismaSchemaPath)) {
|
|
107
|
+
writeFileSync(prismaSchemaPath, `// This is your Prisma schema file,
|
|
108
|
+
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
|
109
|
+
|
|
110
|
+
generator client {
|
|
111
|
+
provider = "prisma-client-js"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
datasource db {
|
|
115
|
+
provider = "postgresql"
|
|
116
|
+
url = env("DATABASE_URL")
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Example model - customize as needed
|
|
120
|
+
model User {
|
|
121
|
+
id String @id @default(cuid())
|
|
122
|
+
name String
|
|
123
|
+
email String @unique
|
|
124
|
+
createdAt DateTime @default(now())
|
|
125
|
+
updatedAt DateTime @updatedAt
|
|
126
|
+
}
|
|
127
|
+
`);
|
|
128
|
+
}
|
|
129
|
+
console.log("\nā
Anvil setup complete!");
|
|
130
|
+
console.log("\nš Next steps:");
|
|
131
|
+
console.log(` 1. cd ${projectName}`);
|
|
132
|
+
console.log(" 2. Update .env.local with your database URL");
|
|
133
|
+
console.log(" 3. Run: anvil db:migrate");
|
|
134
|
+
console.log(" 4. Run: npm run dev");
|
|
135
|
+
console.log(" 5. Visit: http://localhost:3000/admin");
|
|
136
|
+
console.log("\nš Happy coding!");
|
|
137
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "forge-anvil-app",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Create a new Next-anvil application",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-anvil-app": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"dist",
|
|
11
|
+
"bootstrap"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "tsc",
|
|
15
|
+
"prepublishOnly": "npm run build && node ../../scripts/postbuild-create-anvil-app.js"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"create-next-app": "^15.0.0"
|
|
19
|
+
},
|
|
20
|
+
"devDependencies": {
|
|
21
|
+
"@types/node": "^20.0.0",
|
|
22
|
+
"typescript": "^5.0.0"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=18.0.0"
|
|
26
|
+
}
|
|
27
|
+
}
|