next-accelerate 0.0.1 โ 0.2.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.
Potentially problematic release.
This version of next-accelerate might be problematic. Click here for more details.
- package/README.md +44 -24
- package/dist/index.d.ts +4 -0
- package/dist/index.js +882 -3
- package/package.json +10 -4
- package/dist/commands/create-resource.js +0 -31
- package/dist/templates/detail-page.js +0 -17
- package/dist/templates/list-page.js +0 -9
- package/dist/templates/new-page.js +0 -9
- package/dist/utils/fs.js +0 -12
- package/dist/utils/string.js +0 -1
package/README.md
CHANGED
|
@@ -1,46 +1,62 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Next-accelerate
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
*Next-accelerate* is a command-line tool (CLI) that automates repetitive tasks during the development of Next.js projects, already implementing part of the architecture with ready-to-use templates
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+

|
|
6
|
+

|
|
7
|
+

|
|
8
|
+

|
|
9
|
+

|
|
10
|
+

|
|
11
|
+

|
|
12
|
+

|
|
10
13
|
|
|
11
14
|
## Requirements
|
|
12
15
|
|
|
13
|
-
> The
|
|
14
|
-
> Your project
|
|
16
|
+
> The next.js project should follow the development pattern with (App Router).
|
|
17
|
+
> Your project should follow a standardized folder structure using (Nested Layouts or Layout Composition) and Route Groups.
|
|
15
18
|
|
|
16
19
|
## ๐ฆ Installation
|
|
17
20
|
|
|
18
21
|
You can run the CLI **without installing anything globally** using `npx`:
|
|
19
22
|
|
|
20
23
|
```bash
|
|
21
|
-
npx next-accelerate
|
|
24
|
+
npx next-accelerate create singular_resource_name
|
|
22
25
|
|
|
23
26
|
```
|
|
24
27
|
|
|
25
28
|
Or install globally:
|
|
26
29
|
|
|
27
30
|
```bash
|
|
28
|
-
npm install -g next-accelerate
|
|
31
|
+
npm install -g next-accelerate && next-accelerate create singular_resource_name
|
|
29
32
|
|
|
30
33
|
```
|
|
31
34
|
|
|
32
|
-
|
|
35
|
+
Or you can download directly from the repository and build it:
|
|
33
36
|
|
|
34
|
-
|
|
37
|
+
```bash
|
|
38
|
+
git clone https://github.com/brito-response/next-accelerate.git && cd next-accelerate && npm i && npm run build && npm link
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### For now, the CLI only offers the following features
|
|
42
|
+
|
|
43
|
+
- Create pages for a resource.
|
|
35
44
|
|
|
36
45
|
```bash
|
|
37
|
-
next-accelerate
|
|
46
|
+
npx next-accelerate create ingular_resource_name
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- Create forms resouces folders for resources.
|
|
38
50
|
|
|
51
|
+
```bash
|
|
52
|
+
npx next-accelerate create:form ingular_resource_name
|
|
39
53
|
```
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
- Using git commit flag.
|
|
42
56
|
|
|
43
|
-
|
|
57
|
+
```bash
|
|
58
|
+
npx next-accelerate create:form ingular_resource_name --git
|
|
59
|
+
```
|
|
44
60
|
|
|
45
61
|
At the end, you can run:
|
|
46
62
|
|
|
@@ -71,16 +87,20 @@ src/
|
|
|
71
87
|
|
|
72
88
|
## ๐ง Why use it?
|
|
73
89
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
- โฑ๏ธ Saves setup time
|
|
91
|
+
|
|
92
|
+
- ๐ Maintains consistency across projects
|
|
93
|
+
|
|
94
|
+
- ๐งน Avoids repetitive boilerplate code
|
|
95
|
+
- ๐ Ideal for freelancers, squads, and studies
|
|
78
96
|
|
|
79
97
|
## ๐ Technologies
|
|
80
98
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
99
|
+
- Node.js
|
|
100
|
+
|
|
101
|
+
- Next.js
|
|
102
|
+
|
|
103
|
+
- TypeScript
|
|
84
104
|
|
|
85
105
|
---
|
|
86
106
|
|
|
@@ -99,5 +119,5 @@ Contributions are welcome!
|
|
|
99
119
|
|
|
100
120
|
## โจ Author
|
|
101
121
|
|
|
102
|
-
Dveloped by **Neto**
|
|
122
|
+
Dveloped by **Neto** ๐
|
|
103
123
|
If this project helped you, leave a โญ on the repository!
|
package/dist/index.d.ts
ADDED
package/dist/index.js
CHANGED
|
@@ -1,4 +1,883 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
|
|
3
|
+
// src/builders/resource-builder.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import pluralize from "pluralize";
|
|
6
|
+
|
|
7
|
+
// src/templates/forms/create-form.ts
|
|
8
|
+
var formCreateTemplate = (resourceInSingular, resourceInPlural) => `
|
|
9
|
+
"use client";
|
|
10
|
+
|
|
11
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
12
|
+
import { useRouter } from 'next/navigation';
|
|
13
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
14
|
+
import { formSchema, FormSchemaType } from './formredef-scheme';
|
|
15
|
+
import { InputCustom, InputRichTextEditor } from '@/components/Shared/Inputs';
|
|
16
|
+
import { toast } from 'react-toastify';
|
|
17
|
+
import { useEffect, useState } from 'react';
|
|
18
|
+
|
|
19
|
+
export const FormNew${resourceInSingular} = () => {
|
|
20
|
+
const router = useRouter();
|
|
21
|
+
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|
22
|
+
const [photoPreview, setPhotoPreview] = useState<string | null>(null);
|
|
23
|
+
const methods = useForm<FormSchemaType>({ resolver: yupResolver(formSchema), mode: 'onChange', defaultValues: { title: '', content: '' } });
|
|
24
|
+
const { handleSubmit, watch, formState: { isValid, isSubmitting } } = methods;
|
|
25
|
+
const titlePreview = watch('title')
|
|
26
|
+
|
|
27
|
+
const onSubmit = async (data: FormSchemaType) => {
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/posts\`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify(data),
|
|
33
|
+
});
|
|
34
|
+
const post = await response.json();
|
|
35
|
+
|
|
36
|
+
if (response.status === 201 && Boolean(post.postId) && photoFile) {
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
formData.append("photo", photoFile, photoFile.name);
|
|
39
|
+
|
|
40
|
+
const uploadResp = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/images/posts/\${post.postId}\`, { method: "POST", body: formData });
|
|
41
|
+
|
|
42
|
+
if (uploadResp.ok) {
|
|
43
|
+
toast.success("recurso criado com sucesso!");
|
|
44
|
+
router.refresh();
|
|
45
|
+
router.push("/manager");
|
|
46
|
+
}
|
|
47
|
+
else { toast.error("Nenhuma foto foi selecionada."); }
|
|
48
|
+
} else {
|
|
49
|
+
toast.error("Erro ao criar o recurso.");
|
|
50
|
+
}
|
|
51
|
+
} catch (err) {
|
|
52
|
+
toast.error("Erro ao comunicar com o servidor.");
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
57
|
+
const file = e.target.files?.[0] ?? null;
|
|
58
|
+
setPhotoFile(file);
|
|
59
|
+
if (file) {
|
|
60
|
+
const previewUrl = URL.createObjectURL(file);
|
|
61
|
+
setPhotoPreview(previewUrl);
|
|
62
|
+
} else {
|
|
63
|
+
setPhotoPreview(null);
|
|
64
|
+
}
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
useEffect(() => {
|
|
69
|
+
return () => {
|
|
70
|
+
if (photoPreview) {
|
|
71
|
+
URL.revokeObjectURL(photoPreview);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}, [photoPreview]);
|
|
75
|
+
|
|
76
|
+
return (
|
|
77
|
+
<FormProvider {...methods}>
|
|
78
|
+
<form onSubmit={handleSubmit(onSubmit)} className="max-w-6xl mx-auto px-6 py-4 flex flex-col gap-6">
|
|
79
|
+
{/* Preview title */}
|
|
80
|
+
<h1 className="text-3xl font-bold text-(--textcolor)">{titlePreview || 'Pr\xE9via do t\xEDtulo do post'}</h1>
|
|
81
|
+
{/* title */}
|
|
82
|
+
<InputCustom name="title" label="T\xEDtulo do post" required />
|
|
83
|
+
|
|
84
|
+
{photoPreview && (
|
|
85
|
+
<div className="mt-4 w-full h-64 rounded-lg overflow-hidden border border-gray-200">
|
|
86
|
+
<img src={photoPreview} alt="Pr\xE9via da imagem" className="w-full h-full object-cover" />
|
|
87
|
+
</div>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div >
|
|
91
|
+
<label className="block text-sm font-medium text-gray-700">Imagem de exibi\xE7\xE3o do Post</label>
|
|
92
|
+
<input type="file" accept="image/*" onChange={handleFileChange} className="mt-2 border border-dashed rounded-lg p-2 w-full" />
|
|
93
|
+
{photoFile && <p className="text-sm text-green-600 mt-1">{photoFile.name}</p>}
|
|
94
|
+
</div>
|
|
95
|
+
|
|
96
|
+
{/* CONTENT */}
|
|
97
|
+
<InputRichTextEditor name="content" label="Conte\xFAdo" placeholder="Escreva seu post..." />
|
|
98
|
+
|
|
99
|
+
{/* BOTTOM BAR */}
|
|
100
|
+
<div className="sticky bottom-0 border-t pt-4 flex justify-between items-center">
|
|
101
|
+
<span className="text-sm text-gray-500">
|
|
102
|
+
Tem certeza que quer criar esse post?
|
|
103
|
+
</span>
|
|
104
|
+
|
|
105
|
+
<div className="flex gap-3">
|
|
106
|
+
<button type="button" className="text-sm px-4 py-2 rounded-md hover:bg-gray-100">
|
|
107
|
+
Cancelar
|
|
108
|
+
</button>
|
|
109
|
+
<button type="submit" disabled={!isValid || isSubmitting} className="px-6 py-2 bg-blue-700 text-white disabled:bg-gray-600 rounded-md hover:bg-blue-400">
|
|
110
|
+
Criar
|
|
111
|
+
</button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</form>
|
|
115
|
+
</FormProvider>
|
|
116
|
+
);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
`;
|
|
120
|
+
|
|
121
|
+
// src/templates/forms/schems/create-form-scheme.ts
|
|
122
|
+
var formSchemeCreateTemplate = () => `
|
|
123
|
+
import * as yup from 'yup';
|
|
124
|
+
|
|
125
|
+
export const formSchema = yup.object({
|
|
126
|
+
title: yup.string().required('T\xEDtulo \xE9 obrigat\xF3rio'),
|
|
127
|
+
content: yup.string().required('Conte\xFAdo \xE9 obrigat\xF3rio'),
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
export type FormSchemaType = yup.InferType<typeof formSchema>;
|
|
131
|
+
`;
|
|
132
|
+
|
|
133
|
+
// src/templates/forms/delete-form.ts
|
|
134
|
+
var formDeleteTemplate = (resourceInSingular, resourceInPlural) => `
|
|
135
|
+
"use client";
|
|
136
|
+
|
|
137
|
+
import { useRouter } from "next/navigation";
|
|
138
|
+
import { useEffect } from "react";
|
|
139
|
+
import { toast } from "react-toastify";
|
|
140
|
+
|
|
141
|
+
type FormDeleteResourceProps = { resource: string; resourceId: string; routerLinkCancel?: string; };
|
|
142
|
+
|
|
143
|
+
export const FormDeleteResource: React.FC<FormDeleteResourceProps> = ({ resource, resourceId, routerLinkCancel }) => {
|
|
144
|
+
const router = useRouter();
|
|
145
|
+
useEffect(() => {}, [resourceId, resource]);
|
|
146
|
+
|
|
147
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
148
|
+
e.preventDefault();
|
|
149
|
+
try {
|
|
150
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/delete/\${resource}/\${resourceId}\`, { method: "POST" });
|
|
151
|
+
if (response.ok) {
|
|
152
|
+
toast.success("Deletado com sucesso");
|
|
153
|
+
router.push("/manager");
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
toast.error("Erro ao deletar recurso");
|
|
157
|
+
router.push("/${resourceInSingular.toLowerCase()}s");
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
} catch (error) {
|
|
161
|
+
console.log(error);
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
const onCancel = () => {
|
|
166
|
+
if (routerLinkCancel) {
|
|
167
|
+
router.push(routerLinkCancel);
|
|
168
|
+
} else {
|
|
169
|
+
router.back();
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return (
|
|
174
|
+
<form onSubmit={handleSubmit} className="flex justify-center gap-4 py-3">
|
|
175
|
+
<button type="button" onClick={onCancel} className="px-4 py-2 rounded bg-gray-300">
|
|
176
|
+
Cancelar
|
|
177
|
+
</button>
|
|
178
|
+
|
|
179
|
+
<button type="submit" className="px-4 py-2 rounded bg-red-600 text-white">
|
|
180
|
+
Confirmar e Deletar
|
|
181
|
+
</button>
|
|
182
|
+
</form>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
`;
|
|
187
|
+
|
|
188
|
+
// src/templates/forms/update-form.ts
|
|
189
|
+
var formUpdateTemplate = (resourceInSingular, resourceInPlural) => `
|
|
190
|
+
"use client";
|
|
191
|
+
|
|
192
|
+
import { useEffect, useState } from "react";
|
|
193
|
+
import { toast } from "react-toastify";
|
|
194
|
+
import { FormProvider, useForm } from "react-hook-form";
|
|
195
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
196
|
+
import { useRouter } from "next/navigation";
|
|
197
|
+
import { InputCustom, InputRichTextEditor } from "@/components/Shared/Inputs";
|
|
198
|
+
import { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
199
|
+
import { formSchema, FormSchemaType } from "./formredef-scheme";
|
|
200
|
+
|
|
201
|
+
type FormEdit${resourceInSingular}Props = { ${resourceInSingular.toLowerCase()}: ${resourceInSingular}; };
|
|
202
|
+
|
|
203
|
+
export const FormEdit${resourceInSingular}: React.FC<FormEdit${resourceInSingular}Props> = ({ ${resourceInSingular.toLowerCase()} }) => {
|
|
204
|
+
const router = useRouter();
|
|
205
|
+
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|
206
|
+
const [photoPreview, setPhotoPreview] = useState<string | null>(${resourceInSingular}.image ?? null);
|
|
207
|
+
const methods = useForm<any>({
|
|
208
|
+
resolver: yupResolver(formSchema), mode: "onChange",
|
|
209
|
+
defaultValues: { title: ${resourceInSingular.toLocaleLowerCase()}.title, content: ${resourceInSingular.toLocaleLowerCase()}.content, status: ${resourceInSingular.toLocaleLowerCase()}.status, image: ${resourceInSingular.toLocaleLowerCase()}.image },
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
const { handleSubmit, watch, formState: { isValid, isSubmitting, dirtyFields } } = methods;
|
|
213
|
+
const titlePreview = watch("title");
|
|
214
|
+
|
|
215
|
+
const onSubmit = async (data: FormSchemaType) => {
|
|
216
|
+
const payload = Object.fromEntries(Object.entries(data).filter(([key, value]) => value !== undefined && dirtyFields[key as keyof FormSchemaType]));
|
|
217
|
+
if (Object.keys(payload).length === 0 && !photoFile) {
|
|
218
|
+
toast.info("Nenhuma altera\xE7\xE3o para salvar");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/${resourceInSingular}s/\${${resourceInSingular}.${resourceInSingular}Id}\`, {
|
|
224
|
+
method: "${resourceInSingular}",
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify(payload),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!response.ok) {
|
|
230
|
+
toast.error("Erro ao atualizar o ${resourceInSingular.toLocaleLowerCase()}");
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (photoFile) {
|
|
235
|
+
const formData = new FormData();
|
|
236
|
+
formData.append("photo", photoFile);
|
|
237
|
+
const uploadResp = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/images/${resourceInSingular}s/\${${resourceInSingular}.${resourceInSingular}Id}\`, { method: "${resourceInSingular}", body: formData });
|
|
238
|
+
if (!uploadResp.ok) {
|
|
239
|
+
toast.warning("${resourceInSingular} salvo, mas a imagem n\xE3o foi atualizada");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
toast.success("${resourceInSingular} atualizado com sucesso!");
|
|
243
|
+
router.refresh();
|
|
244
|
+
} catch {
|
|
245
|
+
toast.error("Erro ao comunicar com o servidor");
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
250
|
+
const file = e.target.files?.[0] ?? null;
|
|
251
|
+
setPhotoFile(file);
|
|
252
|
+
if (file) {
|
|
253
|
+
setPhotoPreview(URL.createObjectURL(file));
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
useEffect(() => {
|
|
258
|
+
return () => {
|
|
259
|
+
if (photoPreview?.startsWith("blob:")) {
|
|
260
|
+
URL.revokeObjectURL(photoPreview);
|
|
261
|
+
}
|
|
262
|
+
};
|
|
263
|
+
}, [photoPreview]);
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<FormProvider {...methods}>
|
|
267
|
+
<form onSubmit={handleSubmit(onSubmit)} className="max-w-6xl mx-auto px-6 py-4 flex flex-col gap-6">
|
|
268
|
+
<h1 className="text-3xl font-bold">
|
|
269
|
+
{titlePreview || "Pr\xE9via do t\xEDtulo do ${resourceInSingular}"}
|
|
270
|
+
</h1>
|
|
271
|
+
<InputCustom name="title" label="T\xEDtulo do ${resourceInSingular}" />
|
|
272
|
+
{photoPreview && (<img src={\`\${process.env.NEXT_PUBLIC_BACKEND_URL}\${photoPreview}\`} className="h-64 w-full object-cover rounded-lg" alt="Pr\xE9via" />)}
|
|
273
|
+
|
|
274
|
+
<input type="file" accept="image/*" onChange={handleFileChange} />
|
|
275
|
+
<InputRichTextEditor name="content" label="Conte\xFAdo" placeholder="Atualize o conte\xFAdo do ${resourceInSingular}..." />
|
|
276
|
+
|
|
277
|
+
<button type="submit" disabled={!isValid || isSubmitting} className="bg-blue-700 text-white px-6 py-2 rounded-md disabled:opacity-50"> Salvar
|
|
278
|
+
</button>
|
|
279
|
+
</form>
|
|
280
|
+
</FormProvider>
|
|
281
|
+
);
|
|
282
|
+
};
|
|
283
|
+
`;
|
|
284
|
+
|
|
285
|
+
// src/templates/forms/schems/update-form-scheme.ts
|
|
286
|
+
var formSchemeUpdateTemplate = (resourceInSingular, resourceInPlural) => `
|
|
287
|
+
import * as yup from "yup";
|
|
288
|
+
//import { ${resourceInSingular}Status } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Tipo base do formul\xE1rio (PATCH \u2192 tudo opcional)
|
|
292
|
+
*/
|
|
293
|
+
export type FormSchemaType = {
|
|
294
|
+
title?: string;
|
|
295
|
+
content?: string;
|
|
296
|
+
status?: PostStatus;
|
|
297
|
+
image?: string;
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Schema PARCIAL
|
|
302
|
+
*/
|
|
303
|
+
export const formSchema = yup.object({
|
|
304
|
+
title: yup.string().min(3, "O t\xEDtulo deve ter pelo menos 3 caracteres").max(150, "O t\xEDtulo deve ter no m\xE1ximo 150 caracteres").notRequired(),
|
|
305
|
+
content: yup.string().min(10, "O conte\xFAdo deve ter pelo menos 10 caracteres").notRequired(),
|
|
306
|
+
status: yup.mixed<PostStatus>().oneOf(Object.values(PostStatus)).notRequired(),
|
|
307
|
+
image: yup.string().url("A imagem deve ser uma URL v\xE1lida").notRequired(),
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
`;
|
|
311
|
+
|
|
312
|
+
// src/templates/pages/detail-page.ts
|
|
313
|
+
var detailPageTemplate = (resourceInSingular) => `
|
|
314
|
+
interface PageProps {params: {${resourceInSingular}Id: string;};};
|
|
315
|
+
|
|
316
|
+
export default function ${resourceInSingular}DetailPage({ params }: PageProps) {
|
|
317
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = params;
|
|
318
|
+
return (
|
|
319
|
+
<div className="w-full min-h-screen flex flex-col bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
320
|
+
${resourceInSingular} detail: {${resourceInSingular.toLocaleLowerCase()}Id}
|
|
321
|
+
</div>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
`;
|
|
325
|
+
|
|
326
|
+
// src/templates/pages/list-page.ts
|
|
327
|
+
var listPageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
328
|
+
import { ActionButtonsBar } from "@/components/ActionButtonsBar";
|
|
329
|
+
import { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
330
|
+
import { FileCogIcon, FileText, TableConfigIcon } from "lucide-react";
|
|
331
|
+
import Link from "next/link";
|
|
332
|
+
|
|
333
|
+
async function get${resourceInPlural.toLocaleLowerCase()}(): Promise<${resourceInSingular}[]> {
|
|
334
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/${resourceInPlural.toLowerCase()}\`, {
|
|
335
|
+
cache: "no-store",
|
|
336
|
+
});
|
|
337
|
+
if (!response.ok) return [];
|
|
338
|
+
const ${resourceInPlural.toLocaleLowerCase()}: ${resourceInSingular}[] = await response.json();
|
|
339
|
+
return ${resourceInPlural.toLocaleLowerCase()};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
export default async function ${resourceInPlural}Page() {
|
|
343
|
+
const ${resourceInPlural.toLocaleLowerCase()}: ${resourceInSingular}[] = await get${resourceInPlural.toLocaleLowerCase()}();
|
|
344
|
+
|
|
345
|
+
return (
|
|
346
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
347
|
+
<div className="max-w-6xl mx-auto">
|
|
348
|
+
{/* Header */}
|
|
349
|
+
<header className="flex items-center justify-between mb-10">
|
|
350
|
+
<div className="flex items-center gap-3">
|
|
351
|
+
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
|
352
|
+
<FileText className="w-6 h-6" />
|
|
353
|
+
</div>
|
|
354
|
+
<div>
|
|
355
|
+
<h1 className="text-3xl font-semibold">Posts</h1>
|
|
356
|
+
<p className="text-muted-foreground">
|
|
357
|
+
Lista de posts publicados na aplica\xE7\xE3o
|
|
358
|
+
</p>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</header>
|
|
362
|
+
|
|
363
|
+
{/* Posts list */}
|
|
364
|
+
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
365
|
+
{${resourceInPlural.toLocaleLowerCase().toLocaleLowerCase()}.map((${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular}) => (
|
|
366
|
+
<article key={${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id} className="rounded-2xl bg-background p-6 shadow-sm hover:shadow-md transition-shadow flex flex-col justify-between">
|
|
367
|
+
<div className="flex items-start gap-3">
|
|
368
|
+
<div className="p-2 w-full bg-muted text-muted-foreground ">
|
|
369
|
+
<h2 className="font-semibold text-lg leading-snug">
|
|
370
|
+
{${resourceInSingular.toLocaleLowerCase()}.title}
|
|
371
|
+
</h2>
|
|
372
|
+
<div className="flex items-center justify-between">
|
|
373
|
+
<FileText className="w-5 h-5" />
|
|
374
|
+
<Link href={\`/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLowerCase()}Id}/config\`} className="cursor-pointer p-2 hover:bg-amber-400 rounded-2xl">
|
|
375
|
+
<FileCogIcon className="w-5 h-5" />
|
|
376
|
+
</Link>
|
|
377
|
+
|
|
378
|
+
</div>
|
|
379
|
+
|
|
380
|
+
</div>
|
|
381
|
+
</div>
|
|
382
|
+
<ActionButtonsBar linkToEdit={\`/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id}/edit\`} linkToDelete={\`/${resourceInSingular.toLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLowerCase()}Id}/delete\`} />
|
|
383
|
+
</article>
|
|
384
|
+
))}
|
|
385
|
+
</section>
|
|
386
|
+
|
|
387
|
+
{/* Empty state */}
|
|
388
|
+
{posts.length === 0 && (
|
|
389
|
+
<div className="mt-20 text-center text-muted-foreground">
|
|
390
|
+
<FileText className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
|
391
|
+
<p>Nenhum ${resourceInSingular.toLocaleLowerCase()} encontrado</p>
|
|
392
|
+
</div>
|
|
393
|
+
)}
|
|
394
|
+
</div>
|
|
395
|
+
</div>
|
|
396
|
+
);
|
|
397
|
+
};
|
|
398
|
+
`;
|
|
399
|
+
|
|
400
|
+
// src/templates/pages/new-page.ts
|
|
401
|
+
var newPageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
402
|
+
import { FilePlus2, Info } from "lucide-react";
|
|
403
|
+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
404
|
+
import { Session } from "@/utils/session";
|
|
405
|
+
import { getServerSession } from "next-auth";
|
|
406
|
+
import { redirect } from "next/navigation";
|
|
407
|
+
import { FormNewPost } from "@/forms/${resourceInPlural.toLocaleLowerCase()}";
|
|
408
|
+
|
|
409
|
+
export default async function New${resourceInSingular}Page() {
|
|
410
|
+
const session: Session | null = await getServerSession(authOptions);
|
|
411
|
+
if (!session) redirect("/");
|
|
412
|
+
|
|
413
|
+
return (
|
|
414
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] transition-colors duration-500">
|
|
415
|
+
<div className="max-w-4xl mx-auto px-6 py-12">
|
|
416
|
+
<header className="mb-10">
|
|
417
|
+
<div className="flex items-start gap-4">
|
|
418
|
+
<div className="p-3 rounded-2xl bg-primary/10 text-primary">
|
|
419
|
+
<FilePlus2 className="w-6 h-6" />
|
|
420
|
+
</div>
|
|
421
|
+
<div>
|
|
422
|
+
<h1 className="text-3xl font-semibold">Criar novo post</h1>
|
|
423
|
+
<p className="mt-1 text-muted-foreground max-w-2xl">
|
|
424
|
+
Preencha as informa\xE7\xF5es abaixo para publicar um novo post no blog.
|
|
425
|
+
Capriche no t\xEDtulo e no conte\xFAdo para melhorar a leitura e o
|
|
426
|
+
engajamento.
|
|
427
|
+
</p>
|
|
428
|
+
</div>
|
|
429
|
+
</div>
|
|
430
|
+
</header>
|
|
431
|
+
|
|
432
|
+
{/* Info helper */}
|
|
433
|
+
<section className="mb-12 rounded-2xl bg-background shadow-sm p-6 flex gap-3">
|
|
434
|
+
<Info className="w-5 h-5 text-muted-foreground mt-0.5" />
|
|
435
|
+
<p className="text-sm text-muted-foreground">
|
|
436
|
+
Voc\xEA pode editar este ${resourceInSingular.toLowerCase()} depois de publicado. Certifique-se de revisar
|
|
437
|
+
o conte\xFAdo antes de salvar.
|
|
438
|
+
</p>
|
|
439
|
+
</section>
|
|
440
|
+
|
|
441
|
+
<FormNew${resourceInSingular} />
|
|
442
|
+
</div>
|
|
443
|
+
</div>
|
|
444
|
+
);
|
|
445
|
+
}
|
|
446
|
+
`;
|
|
447
|
+
|
|
448
|
+
// src/templates/inputs/input-template.ts
|
|
449
|
+
var inputTemplate = () => `
|
|
450
|
+
"use client";
|
|
451
|
+
import { useFormContext, Controller } from 'react-hook-form';
|
|
452
|
+
|
|
453
|
+
interface InputProps {
|
|
454
|
+
name: string;
|
|
455
|
+
label: string;
|
|
456
|
+
type?: string;
|
|
457
|
+
required?: boolean;
|
|
458
|
+
ishidden?: boolean;
|
|
459
|
+
pattern?: RegExp;
|
|
460
|
+
placeholder?: string;
|
|
461
|
+
asDate?: boolean;
|
|
462
|
+
multiline?: boolean;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
export const InputCustom: React.FC<InputProps> = ({ name, label, type = "text", required, pattern, placeholder, asDate, ishidden, multiline = false }) => {
|
|
466
|
+
const { control, formState: { errors } } = useFormContext();
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<div className="flex flex-col gap-2">
|
|
470
|
+
{!ishidden && (
|
|
471
|
+
<label htmlFor={name} className="text-sm font-medium text-[--text-label]">
|
|
472
|
+
{label}:
|
|
473
|
+
</label>
|
|
474
|
+
)}
|
|
475
|
+
|
|
476
|
+
<Controller
|
|
477
|
+
control={control}
|
|
478
|
+
name={name}
|
|
479
|
+
rules={{
|
|
480
|
+
required: !ishidden && required ? "Campo obrigat\xF3rio" : false,
|
|
481
|
+
pattern: pattern ? { value: pattern, message: "Formato inv\xE1lido" } : undefined,
|
|
482
|
+
}}
|
|
483
|
+
render={({ field }) => {
|
|
484
|
+
const value = asDate && typeof field.value === "string" ? field.value.split("T")[0] : field.value ?? "";
|
|
485
|
+
const baseClasses = "w-full p-2 px-3 mt-1 border shadow-sm outline-none transition-all focus:border-blue-300 focus:ring-1 focus:ring-blue-300";
|
|
486
|
+
const errorClass = errors[name] ? "border-red-400" : "border-gray-300";
|
|
487
|
+
|
|
488
|
+
if (multiline) {
|
|
489
|
+
return (
|
|
490
|
+
<textarea {...field} id={name} rows={5} placeholder={placeholder} value={value} onChange={field.onChange} className={\`\${baseClasses} \${errorClass} rounded-xl resize-y min-h-30\`}/>
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return (
|
|
495
|
+
<input {...field} id={name} type={type} placeholder={ishidden ? "" : placeholder} hidden={ishidden} aria-hidden={ishidden} value={value} onChange={(e) => {
|
|
496
|
+
if (asDate) {
|
|
497
|
+
const v = e.target.value;
|
|
498
|
+
if (!v) {
|
|
499
|
+
field.onChange(null);
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const date = new Date(v);
|
|
503
|
+
if (isNaN(date.getTime())) return;
|
|
504
|
+
|
|
505
|
+
field.onChange(date.toISOString());
|
|
506
|
+
} else {
|
|
507
|
+
field.onChange(e);
|
|
508
|
+
}
|
|
509
|
+
}}
|
|
510
|
+
className={\`\${baseClasses} \${errorClass} rounded-full\`}
|
|
511
|
+
/>
|
|
512
|
+
);
|
|
513
|
+
}}
|
|
514
|
+
/>
|
|
515
|
+
{errors[name]?.message && (
|
|
516
|
+
<p className="text-sm text-red-500">
|
|
517
|
+
{errors[name]?.message as string}
|
|
518
|
+
</p>
|
|
519
|
+
)}
|
|
520
|
+
</div>
|
|
521
|
+
);
|
|
522
|
+
};
|
|
523
|
+
`;
|
|
524
|
+
|
|
525
|
+
// src/utils/fs.ts
|
|
526
|
+
import fs from "fs";
|
|
527
|
+
var createDir = (dirPath) => {
|
|
528
|
+
if (!fs.existsSync(dirPath)) {
|
|
529
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
530
|
+
}
|
|
531
|
+
;
|
|
532
|
+
};
|
|
533
|
+
var createFile = (filePath, content) => {
|
|
534
|
+
if (!fs.existsSync(filePath)) {
|
|
535
|
+
fs.writeFileSync(filePath, content.trimStart());
|
|
536
|
+
}
|
|
537
|
+
};
|
|
538
|
+
function pathExists(p) {
|
|
539
|
+
return fs.existsSync(p);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// src/utils/string.ts
|
|
543
|
+
var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
544
|
+
|
|
545
|
+
// src/templates/pages/update-page.ts
|
|
546
|
+
var updatePageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
547
|
+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
548
|
+
import { FormEdit${resourceInSingular} } from "@/forms/${resourceInPlural.toLocaleLowerCase()}";
|
|
549
|
+
import { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
550
|
+
import { Session } from "@/utils/session";
|
|
551
|
+
import { getServerSession } from "next-auth";
|
|
552
|
+
import { redirect } from "next/navigation";
|
|
553
|
+
|
|
554
|
+
interface PageProps { params: { ${resourceInSingular.toLocaleLowerCase()}Id: string; }; };
|
|
555
|
+
|
|
556
|
+
async function get${resourceInSingular}ById(${resourceInSingular.toLocaleLowerCase()}Id: string, token: string): Promise< ${resourceInSingular} | null> {
|
|
557
|
+
try {
|
|
558
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}Id}\`,
|
|
559
|
+
{
|
|
560
|
+
method: "GET",
|
|
561
|
+
cache: "no-store",
|
|
562
|
+
headers: {
|
|
563
|
+
Authorization: \`Bearer \${token}\`,
|
|
564
|
+
},
|
|
565
|
+
}
|
|
566
|
+
);
|
|
567
|
+
if (!response.ok) { return null; };
|
|
568
|
+
const post: Post = await response.json();
|
|
569
|
+
return post;
|
|
570
|
+
} catch (error) {
|
|
571
|
+
console.error("Erro ao buscar post:", error);
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
};
|
|
575
|
+
|
|
576
|
+
export default async function ${resourceInSingular}EditPage({ params }: PageProps) {
|
|
577
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = await params;
|
|
578
|
+
const session: Session | null = await getServerSession(authOptions);
|
|
579
|
+
if (!session) redirect("/");
|
|
580
|
+
const ${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular} | null = await get${resourceInSingular}ById(${resourceInSingular.toLocaleLowerCase()}Id, session.accessToken);
|
|
581
|
+
return (
|
|
582
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
583
|
+
<div className="max-w-3xl mx-auto flex flex-col gap-8">
|
|
584
|
+
|
|
585
|
+
{${resourceInSingular.toLocaleLowerCase()} ? (
|
|
586
|
+
<>
|
|
587
|
+
{/* Informa\xE7\xF5es do post */}
|
|
588
|
+
<section className="bg-white/70 dark:bg-black/20 rounded-xl p-6 shadow">
|
|
589
|
+
<h1 className="text-2xl font-semibold">Editar ${resourceInSingular}</h1>
|
|
590
|
+
<p className="text-sm opacity-70 mt-1">
|
|
591
|
+
Este ${resourceInSingular} foi \u2022 Criado em {${resourceInSingular.toLocaleLowerCase()}.createdAt.toString()}
|
|
592
|
+
</p>
|
|
593
|
+
</section>
|
|
594
|
+
|
|
595
|
+
{/* Formul\xE1rio */}
|
|
596
|
+
<FormEdit${resourceInSingular} ${resourceInSingular.toLowerCase()}={${resourceInSingular.toLocaleLowerCase()}} />
|
|
597
|
+
</>
|
|
598
|
+
) : (
|
|
599
|
+
<section className="bg-white/70 dark:bg-black/20 rounded-xl p-6 shadow text-center">
|
|
600
|
+
<h1 className="text-xl font-semibold">
|
|
601
|
+
Post n\xE3o encontrado
|
|
602
|
+
</h1>
|
|
603
|
+
<p className="text-sm opacity-70 mt-2">
|
|
604
|
+
O ${resourceInSingular} que voc\xEA est\xE1 tentando editar n\xE3o existe ou foi removido.
|
|
605
|
+
</p>
|
|
606
|
+
</section>
|
|
607
|
+
)}
|
|
608
|
+
|
|
609
|
+
</div>
|
|
610
|
+
</div>
|
|
611
|
+
);
|
|
612
|
+
};
|
|
613
|
+
|
|
614
|
+
`;
|
|
615
|
+
|
|
616
|
+
// src/templates/pages/delete-page.ts
|
|
617
|
+
var deletePageTemplate = (resourceInSingular) => `
|
|
618
|
+
import { FormDeleteResource } from "@/forms/shared";
|
|
619
|
+
|
|
620
|
+
interface PageProps { params: { ${resourceInSingular.toLocaleLowerCase()}Id: string; }; };
|
|
621
|
+
export default async function ${resourceInSingular}DeletePage({ params }: PageProps) {
|
|
622
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = await params;
|
|
623
|
+
|
|
624
|
+
return <div className="w-full min-h-screen flex flex-col bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
625
|
+
<h2 className="text-center">Tem certeza que vc quer deletar esse ${resourceInSingular}?</h2>
|
|
626
|
+
<FormDeleteResource resource={"${resourceInSingular.toLocaleLowerCase()}"} resourceId={${resourceInSingular.toLocaleLowerCase()}Id} />
|
|
627
|
+
</div>;
|
|
628
|
+
}
|
|
629
|
+
`;
|
|
630
|
+
|
|
631
|
+
// src/utils/services/git.service.ts
|
|
632
|
+
import { execSync } from "child_process";
|
|
633
|
+
function gitCommit(message) {
|
|
634
|
+
if (!message) return;
|
|
635
|
+
const safeMessage = String.raw`${message.replace(/(["`$\\])/g, "\\$1")}`;
|
|
636
|
+
try {
|
|
637
|
+
execSync("git add .", { stdio: "ignore" });
|
|
638
|
+
execSync(`git commit -m "${safeMessage}"`, { stdio: "ignore" });
|
|
639
|
+
} catch (error) {
|
|
640
|
+
console.error("Failed to commit changes:", error);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
// src/utils/services/install-dependences.service.ts
|
|
645
|
+
import { execSync as execSync2 } from "child_process";
|
|
646
|
+
var DependencyInstaller = class _DependencyInstaller {
|
|
647
|
+
constructor() {
|
|
648
|
+
this.hasInstalled = false;
|
|
649
|
+
}
|
|
650
|
+
static getInstance() {
|
|
651
|
+
if (!_DependencyInstaller.instance) {
|
|
652
|
+
_DependencyInstaller.instance = new _DependencyInstaller();
|
|
653
|
+
}
|
|
654
|
+
return _DependencyInstaller.instance;
|
|
655
|
+
}
|
|
656
|
+
async install() {
|
|
657
|
+
if (this.hasInstalled) return;
|
|
658
|
+
this.hasInstalled = true;
|
|
659
|
+
try {
|
|
660
|
+
execSync2(`
|
|
661
|
+
npm i lucide-react &&
|
|
662
|
+
npm install next-auth &&
|
|
663
|
+
npm install jwt-decode &&
|
|
664
|
+
npm install --save-dev @types/jwt-decode &&
|
|
665
|
+
npm install clsx &&
|
|
666
|
+
npm install react-hook-form &&
|
|
667
|
+
npm install yup @hookform/resolvers &&
|
|
668
|
+
npm install -D @types/yup &&
|
|
669
|
+
npm install react-toastify
|
|
670
|
+
`, { stdio: "inherit" });
|
|
671
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
672
|
+
} catch (error) {
|
|
673
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
674
|
+
}
|
|
675
|
+
;
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
|
|
679
|
+
// src/builders/resource-builder.ts
|
|
680
|
+
var NextResourceBuilder = class {
|
|
681
|
+
constructor(inputName, options) {
|
|
682
|
+
this.inputName = inputName;
|
|
683
|
+
this.commitQueue = [];
|
|
684
|
+
this.options = options;
|
|
685
|
+
this.resource = pluralize(inputName.toLowerCase());
|
|
686
|
+
this.singular = pluralize.singular(this.resource);
|
|
687
|
+
}
|
|
688
|
+
createCommit(message) {
|
|
689
|
+
if (!this.options?.git) return;
|
|
690
|
+
gitCommit(message);
|
|
691
|
+
}
|
|
692
|
+
registerCommit(message) {
|
|
693
|
+
if (!this.options?.git) return;
|
|
694
|
+
this.commitQueue.push(message);
|
|
695
|
+
}
|
|
696
|
+
setBasePath() {
|
|
697
|
+
this.basePath = path.join(process.cwd(), "src/app/(privates)", this.resource);
|
|
698
|
+
createDir(this.basePath);
|
|
699
|
+
return this;
|
|
700
|
+
}
|
|
701
|
+
setBasePathForForm() {
|
|
702
|
+
this.basePath = path.join(process.cwd(), "src/forms");
|
|
703
|
+
createDir(this.basePath);
|
|
704
|
+
return this;
|
|
705
|
+
}
|
|
706
|
+
setBasePathForComponents() {
|
|
707
|
+
this.basePath = path.join(process.cwd(), "src/components");
|
|
708
|
+
createDir(this.basePath);
|
|
709
|
+
return this;
|
|
710
|
+
}
|
|
711
|
+
createComponentInputCustom() {
|
|
712
|
+
const componentPath = path.join(this.basePath, "Inputs/InputCustom");
|
|
713
|
+
createDir(componentPath);
|
|
714
|
+
createFile(path.join(componentPath, "index.tsx"), inputTemplate());
|
|
715
|
+
if (this.options?.git) this.createCommit(`feat(${this.resource}): add input custom component`);
|
|
716
|
+
return this;
|
|
717
|
+
}
|
|
718
|
+
createListPage() {
|
|
719
|
+
createFile(path.join(this.basePath, "page.tsx"), listPageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
720
|
+
return this;
|
|
721
|
+
}
|
|
722
|
+
createDetailPage() {
|
|
723
|
+
const detailDir = path.join(this.basePath, `[${this.singular}Id]`);
|
|
724
|
+
createDir(detailDir);
|
|
725
|
+
createFile(path.join(detailDir, "page.tsx"), detailPageTemplate(capitalize(this.singular)));
|
|
726
|
+
const editDir = path.join(detailDir, "edit");
|
|
727
|
+
createDir(editDir);
|
|
728
|
+
createFile(path.join(editDir, "page.tsx"), updatePageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
729
|
+
const deleteDir = path.join(detailDir, "delete");
|
|
730
|
+
createDir(deleteDir);
|
|
731
|
+
createFile(path.join(deleteDir, "page.tsx"), deletePageTemplate(capitalize(this.singular)));
|
|
732
|
+
this.registerCommit(`feat(${this.resource}): add all pages for detail view`);
|
|
733
|
+
return this;
|
|
734
|
+
}
|
|
735
|
+
createNewPage() {
|
|
736
|
+
const dir = path.join(this.basePath, "new");
|
|
737
|
+
createDir(dir);
|
|
738
|
+
createFile(path.join(dir, "page.tsx"), newPageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
739
|
+
return this;
|
|
740
|
+
}
|
|
741
|
+
createCrudForm() {
|
|
742
|
+
const sharedPath = path.join(this.basePath, "shared");
|
|
743
|
+
const deletePath = path.join(sharedPath, "FormDelete");
|
|
744
|
+
if (!pathExists(deletePath)) {
|
|
745
|
+
createDir(sharedPath);
|
|
746
|
+
createDir(deletePath);
|
|
747
|
+
createFile(path.join(deletePath, "index.tsx"), formDeleteTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
748
|
+
createFile(path.join(sharedPath, "index.ts"), `export { FormDeleteResource } from "./FormDelete";
|
|
749
|
+
`);
|
|
750
|
+
}
|
|
751
|
+
const resourcePath = path.join(this.basePath, this.resource);
|
|
752
|
+
createDir(resourcePath);
|
|
753
|
+
DependencyInstaller.getInstance().install();
|
|
754
|
+
const formNewPath = path.join(resourcePath, "FormNew");
|
|
755
|
+
createDir(formNewPath);
|
|
756
|
+
createFile(path.join(formNewPath, "index.tsx"), formCreateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
757
|
+
createFile(path.join(formNewPath, "form-scheme.ts"), formSchemeCreateTemplate());
|
|
758
|
+
const formEditPath = path.join(resourcePath, "FormEdit");
|
|
759
|
+
createDir(formEditPath);
|
|
760
|
+
createFile(path.join(formEditPath, "index.tsx"), formUpdateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
761
|
+
createFile(path.join(formEditPath, "form-scheme.ts"), formSchemeUpdateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
762
|
+
createFile(path.join(resourcePath, "index.ts"), `
|
|
763
|
+
export { FormNew${capitalize(this.singular)} } from "./FormNew";
|
|
764
|
+
|
|
765
|
+
export { FormEdit${capitalize(this.singular)} } from "./FormEdit";
|
|
766
|
+
`);
|
|
767
|
+
this.registerCommit(`feat(${this.resource}): created crud form components`);
|
|
768
|
+
return this;
|
|
769
|
+
}
|
|
770
|
+
build() {
|
|
771
|
+
if (!this.options?.git) return;
|
|
772
|
+
for (const message of this.commitQueue) {
|
|
773
|
+
gitCommit(message);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
};
|
|
777
|
+
|
|
778
|
+
// src/utils/guards/next-verify.guard.ts
|
|
779
|
+
import fs2 from "fs";
|
|
780
|
+
import path2 from "path";
|
|
781
|
+
function nextProjectGuardSimple() {
|
|
782
|
+
const cwd = process.cwd();
|
|
783
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
784
|
+
if (!fs2.existsSync(pkgPath)) {
|
|
785
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNenhum package.json encontrado neste diret\xF3rio.");
|
|
786
|
+
process.exit(1);
|
|
787
|
+
}
|
|
788
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
789
|
+
const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next;
|
|
790
|
+
if (!hasNext) {
|
|
791
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mExecute dentro de um projeto Next.js");
|
|
792
|
+
process.exit(1);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/commands/create-resource.ts
|
|
797
|
+
function createResource(inputName, options) {
|
|
798
|
+
nextProjectGuardSimple();
|
|
799
|
+
if (!inputName) {
|
|
800
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInforme o nome do recurso");
|
|
801
|
+
process.exit(1);
|
|
802
|
+
}
|
|
803
|
+
const builder = new NextResourceBuilder(inputName, options);
|
|
804
|
+
builder.setBasePath().createListPage().createDetailPage().createNewPage().build();
|
|
805
|
+
console.log(`Recurso "${inputName}" criado \x1B[32m\u2714 Sucesso\x1B[0m`);
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// src/commands/create-form-resource.ts
|
|
809
|
+
function createFormForResource(inputName, options) {
|
|
810
|
+
nextProjectGuardSimple();
|
|
811
|
+
if (!inputName) {
|
|
812
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInforme o nome do recurso");
|
|
813
|
+
process.exit(1);
|
|
814
|
+
}
|
|
815
|
+
const builder = new NextResourceBuilder(inputName, options);
|
|
816
|
+
builder.setBasePathForComponents().createComponentInputCustom().setBasePathForForm().createCrudForm().build();
|
|
817
|
+
console.log(`Form para o recurso "${inputName}" criado \x1B[32m\u2714 Sucesso\x1B[0m`);
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
// src/utils/interceptors/args.interceptor.ts
|
|
821
|
+
var ALLOWED_FLAGS = /* @__PURE__ */ new Set(["--git"]);
|
|
822
|
+
function useArgsInterceptor(args) {
|
|
823
|
+
const [, , ...input] = args;
|
|
824
|
+
if (input.length === 0) {
|
|
825
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNo command was entered.");
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
const command = input[0];
|
|
829
|
+
if (command.startsWith("-") && command !== "-help") {
|
|
830
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInvalid command");
|
|
831
|
+
process.exit(1);
|
|
832
|
+
}
|
|
833
|
+
const rest = input.slice(1);
|
|
834
|
+
let resource;
|
|
835
|
+
const flags = [];
|
|
836
|
+
for (const token of rest) {
|
|
837
|
+
if (token.startsWith("-")) {
|
|
838
|
+
if (ALLOWED_FLAGS.has(token)) {
|
|
839
|
+
flags.push(token);
|
|
840
|
+
continue;
|
|
841
|
+
}
|
|
842
|
+
if (!resource) {
|
|
843
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInvalid resource name");
|
|
844
|
+
process.exit(1);
|
|
845
|
+
}
|
|
846
|
+
console.error(`\x1B[31m \u2716 Erro \x1B[0mUnknown flag: ${token}`);
|
|
847
|
+
process.exit(1);
|
|
848
|
+
}
|
|
849
|
+
if (!resource) {
|
|
850
|
+
resource = token;
|
|
851
|
+
continue;
|
|
852
|
+
}
|
|
853
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNo additional arguments are allowed.");
|
|
854
|
+
process.exit(1);
|
|
855
|
+
}
|
|
856
|
+
return { command, resource, flags };
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
// src/index.ts
|
|
860
|
+
function main(args) {
|
|
861
|
+
const { command, resource, flags } = useArgsInterceptor(args);
|
|
862
|
+
const useGit = flags.includes("--git");
|
|
863
|
+
switch (command) {
|
|
864
|
+
case "create":
|
|
865
|
+
createResource(resource, { git: useGit });
|
|
866
|
+
break;
|
|
867
|
+
case "create:form":
|
|
868
|
+
createFormForResource(resource, { git: useGit });
|
|
869
|
+
break;
|
|
870
|
+
case "-help":
|
|
871
|
+
console.log("commands available in the cli:");
|
|
872
|
+
console.log(" create <resource-name> - creates all folders for a new resource.");
|
|
873
|
+
console.log(" create:form <resource-name> - creates a new form for the resource");
|
|
874
|
+
break;
|
|
875
|
+
default:
|
|
876
|
+
console.log("command unavailable in the cli...");
|
|
877
|
+
}
|
|
878
|
+
;
|
|
879
|
+
}
|
|
880
|
+
main(process.argv);
|
|
881
|
+
export {
|
|
882
|
+
main
|
|
883
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "next-accelerate",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "cli to speed up feature creation in Next.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"next-accelerate": "dist/index.js"
|
|
@@ -10,8 +10,9 @@
|
|
|
10
10
|
"dist"
|
|
11
11
|
],
|
|
12
12
|
"scripts": {
|
|
13
|
-
"build": "
|
|
14
|
-
"dev": "tsc -w"
|
|
13
|
+
"build": "tsup src/index.ts --format esm --dts --out-dir dist --clean && chmod +x dist/index.js",
|
|
14
|
+
"dev": "tsc -w",
|
|
15
|
+
"test": "jest"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [],
|
|
17
18
|
"author": "Clodoaldo Neto",
|
|
@@ -19,9 +20,14 @@
|
|
|
19
20
|
"devDependencies": {
|
|
20
21
|
"@types/node": "^20.11.0",
|
|
21
22
|
"@types/pluralize": "^0.0.33",
|
|
23
|
+
"ts-node": "^10.9.2",
|
|
24
|
+
"tsup": "^8.5.1",
|
|
22
25
|
"typescript": "^5.9.3"
|
|
23
26
|
},
|
|
24
27
|
"dependencies": {
|
|
25
|
-
"
|
|
28
|
+
"@types/jest": "^30.0.0",
|
|
29
|
+
"jest": "^30.2.0",
|
|
30
|
+
"pluralize": "^8.0.0",
|
|
31
|
+
"ts-jest": "^29.4.6"
|
|
26
32
|
}
|
|
27
33
|
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import path from "path";
|
|
2
|
-
import fs from "fs";
|
|
3
|
-
import pluralize from "pluralize";
|
|
4
|
-
import { createDir, createFile } from "../utils/fs.js";
|
|
5
|
-
import { capitalize } from "../utils/string.js";
|
|
6
|
-
import { listPageTemplate } from "../templates/list-page.js";
|
|
7
|
-
import { detailPageTemplate } from "../templates/detail-page.js";
|
|
8
|
-
import { newPageTemplate } from "../templates/new-page.js";
|
|
9
|
-
export function createResource(inputName) {
|
|
10
|
-
if (!inputName) {
|
|
11
|
-
console.error("\x1b[32mโ Sucesso\x1b[0m" + "Informe o nome do recurso. Ex: user");
|
|
12
|
-
process.exit(1);
|
|
13
|
-
}
|
|
14
|
-
const appDir = path.join(process.cwd(), "src/app");
|
|
15
|
-
if (!fs.existsSync(appDir)) {
|
|
16
|
-
console.error("\x1b[31mโ Erro\x1b[0m" + "Execute dentro de um projeto Next.js");
|
|
17
|
-
process.exit(1);
|
|
18
|
-
}
|
|
19
|
-
const resource = pluralize(inputName.toLowerCase());
|
|
20
|
-
const singular = pluralize.singular(resource);
|
|
21
|
-
const BASE_PATH = path.join(process.cwd(), "src/app/(privates)", resource);
|
|
22
|
-
createDir(BASE_PATH);
|
|
23
|
-
createFile(path.join(BASE_PATH, "page.tsx"), listPageTemplate(capitalize(resource)));
|
|
24
|
-
const detailDir = path.join(BASE_PATH, `[${singular}Id]`);
|
|
25
|
-
createDir(detailDir);
|
|
26
|
-
createFile(path.join(detailDir, "page.tsx"), detailPageTemplate(capitalize(singular)));
|
|
27
|
-
const newDir = path.join(BASE_PATH, "new");
|
|
28
|
-
createDir(newDir);
|
|
29
|
-
createFile(path.join(newDir, "page.tsx"), newPageTemplate(capitalize(singular)));
|
|
30
|
-
console.log(`Recurso "${resource}" criado com sucesso` + "\x1b[32mโ Sucesso\x1b[0m");
|
|
31
|
-
}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
export const detailPageTemplate = (singular) => `
|
|
2
|
-
interface PageProps {
|
|
3
|
-
params: {
|
|
4
|
-
${singular}Id: string;
|
|
5
|
-
};
|
|
6
|
-
}
|
|
7
|
-
|
|
8
|
-
export default function ${singular}DetailPage({ params }: PageProps) {
|
|
9
|
-
const { ${singular}Id } = params;
|
|
10
|
-
|
|
11
|
-
return (
|
|
12
|
-
<div className="w-full min-h-screen flex flex-col bg-(--bg-section-100) p-10 transition-colors duration-500">
|
|
13
|
-
${singular} detail: {${singular}Id}
|
|
14
|
-
</div>
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
`;
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export const newPageTemplate = (singular) => `
|
|
2
|
-
export default function New${singular}Page() {
|
|
3
|
-
return (
|
|
4
|
-
<div className="w-full min-h-screen flex flex-col bg-(--bg-section-100) p-10 transition-colors duration-500">
|
|
5
|
-
create new ${singular}
|
|
6
|
-
</div>
|
|
7
|
-
);
|
|
8
|
-
}
|
|
9
|
-
`;
|
package/dist/utils/fs.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import fs from "fs";
|
|
2
|
-
export const createDir = (dirPath) => {
|
|
3
|
-
if (!fs.existsSync(dirPath)) {
|
|
4
|
-
fs.mkdirSync(dirPath, { recursive: true });
|
|
5
|
-
}
|
|
6
|
-
;
|
|
7
|
-
};
|
|
8
|
-
export const createFile = (filePath, content) => {
|
|
9
|
-
if (!fs.existsSync(filePath)) {
|
|
10
|
-
fs.writeFileSync(filePath, content.trimStart());
|
|
11
|
-
}
|
|
12
|
-
};
|
package/dist/utils/string.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|