next-accelerate 1.0.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/LICENSE +21 -0
- package/README.md +262 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3217 -0
- package/package.json +33 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3217 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/builders/resource-builder.ts
|
|
4
|
+
import path4 from "path";
|
|
5
|
+
|
|
6
|
+
// src/templates/forms/create-form.ts
|
|
7
|
+
var formCreateTemplate = (resourceInSingular, resourceInPlural) => `
|
|
8
|
+
"use client";
|
|
9
|
+
|
|
10
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
11
|
+
import { useRouter } from 'next/navigation';
|
|
12
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
13
|
+
import { formSchema, FormSchemaType } from './form-scheme';
|
|
14
|
+
import { InputCustom, InputRichTextEditor } from '@/components/Shared/Inputs';
|
|
15
|
+
import { toast } from 'react-toastify';
|
|
16
|
+
import { useEffect, useState } from 'react';
|
|
17
|
+
import { delay } from '@/utils/utils';
|
|
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/${resourceInPlural.toLocaleLowerCase()}\`, {
|
|
30
|
+
method: "POST",
|
|
31
|
+
headers: { "Content-Type": "application/json" },
|
|
32
|
+
body: JSON.stringify(data),
|
|
33
|
+
});
|
|
34
|
+
const ${resourceInSingular.toLocaleLowerCase()} = await response.json();
|
|
35
|
+
|
|
36
|
+
if (response.status === 201 && Boolean(${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id) && photoFile) {
|
|
37
|
+
const formData = new FormData();
|
|
38
|
+
formData.append("photo", photoFile, photoFile.name);
|
|
39
|
+
toast.success("encaminhando imagem ...");
|
|
40
|
+
await delay(3000);
|
|
41
|
+
const uploadResp = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/images/posts/\${post.postId}\`, { method: "POST", body: formData });
|
|
42
|
+
|
|
43
|
+
if (uploadResp.ok) {
|
|
44
|
+
toast.success("Resource created successfully!");
|
|
45
|
+
router.refresh();
|
|
46
|
+
router.push("/manager");
|
|
47
|
+
}
|
|
48
|
+
else { toast.error("No photos were selected."); }
|
|
49
|
+
} else {
|
|
50
|
+
toast.error("Erro ao criar o recurso.");
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
toast.error("Error communicating with the server.");
|
|
54
|
+
}
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
58
|
+
const file = e.target.files?.[0] ?? null;
|
|
59
|
+
setPhotoFile(file);
|
|
60
|
+
if (file) {
|
|
61
|
+
const previewUrl = URL.createObjectURL(file);
|
|
62
|
+
setPhotoPreview(previewUrl);
|
|
63
|
+
} else {
|
|
64
|
+
setPhotoPreview(null);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
return () => {
|
|
71
|
+
if (photoPreview) {
|
|
72
|
+
URL.revokeObjectURL(photoPreview);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
}, [photoPreview]);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<FormProvider {...methods}>
|
|
79
|
+
<form onSubmit={handleSubmit(onSubmit)} className="max-w-6xl mx-auto px-6 py-4 flex flex-col gap-6">
|
|
80
|
+
{/* Preview title */}
|
|
81
|
+
<h1 className="text-3xl font-bold text-(--textcolor)">{titlePreview || 'Pr\xE9via do t\xEDtulo do post'}</h1>
|
|
82
|
+
{/* title */}
|
|
83
|
+
<InputCustom name="title" label="T\xEDtulo do post" required />
|
|
84
|
+
|
|
85
|
+
{photoPreview && (
|
|
86
|
+
<div className="mt-4 w-full h-64 rounded-lg overflow-hidden border border-gray-200">
|
|
87
|
+
<img src={photoPreview} alt="Pr\xE9via da imagem" className="w-full h-full object-cover" />
|
|
88
|
+
</div>
|
|
89
|
+
)}
|
|
90
|
+
|
|
91
|
+
<div >
|
|
92
|
+
<label className="block text-sm font-medium text-gray-700">Imagem de exibi\xE7\xE3o do Post</label>
|
|
93
|
+
<input type="file" accept="image/*" onChange={handleFileChange} className="mt-2 border border-dashed rounded-lg p-2 w-full" />
|
|
94
|
+
{photoFile && <p className="text-sm text-green-600 mt-1">{photoFile.name}</p>}
|
|
95
|
+
</div>
|
|
96
|
+
|
|
97
|
+
{/* CONTENT */}
|
|
98
|
+
<InputRichTextEditor name="content" label="Conte\xFAdo" placeholder="Escreva seu post..." />
|
|
99
|
+
|
|
100
|
+
{/* BOTTOM BAR */}
|
|
101
|
+
<div className="sticky bottom-0 border-t pt-4 flex justify-between items-center">
|
|
102
|
+
<span className="text-sm text-gray-500">
|
|
103
|
+
Tem certeza que quer criar esse post?
|
|
104
|
+
</span>
|
|
105
|
+
|
|
106
|
+
<div className="flex gap-3">
|
|
107
|
+
<button type="button" className="text-sm px-4 py-2 rounded-md hover:bg-gray-100">
|
|
108
|
+
Cancelar
|
|
109
|
+
</button>
|
|
110
|
+
<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">
|
|
111
|
+
Criar
|
|
112
|
+
</button>
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
</form>
|
|
116
|
+
</FormProvider>
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
`;
|
|
121
|
+
|
|
122
|
+
// src/templates/forms/schems/create-form-scheme.ts
|
|
123
|
+
var formSchemeCreateTemplate = () => `
|
|
124
|
+
import * as yup from 'yup';
|
|
125
|
+
|
|
126
|
+
export const formSchema = yup.object({
|
|
127
|
+
title: yup.string().required('T\xEDtulo \xE9 obrigat\xF3rio'),
|
|
128
|
+
content: yup.string().required('Conte\xFAdo \xE9 obrigat\xF3rio'),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export type FormSchemaType = yup.InferType<typeof formSchema>;
|
|
132
|
+
`;
|
|
133
|
+
|
|
134
|
+
// src/templates/forms/delete-form.ts
|
|
135
|
+
var formDeleteTemplate = (resourceInSingular, resourceInPlural) => `
|
|
136
|
+
"use client";
|
|
137
|
+
|
|
138
|
+
import { useRouter } from "next/navigation";
|
|
139
|
+
import { useEffect } from "react";
|
|
140
|
+
import { toast } from "react-toastify";
|
|
141
|
+
|
|
142
|
+
type FormDeleteResourceProps = { resource: string; resourceId: string; routerLinkCancel?: string; };
|
|
143
|
+
|
|
144
|
+
export const FormDeleteResource: React.FC<FormDeleteResourceProps> = ({ resource, resourceId, routerLinkCancel }) => {
|
|
145
|
+
const router = useRouter();
|
|
146
|
+
useEffect(() => {}, [resourceId, resource]);
|
|
147
|
+
|
|
148
|
+
const handleSubmit = async (e: React.FormEvent) => {
|
|
149
|
+
e.preventDefault();
|
|
150
|
+
try {
|
|
151
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/delete/\${resource}/\${resourceId}\`, { method: "POST" });
|
|
152
|
+
if (response.ok) {
|
|
153
|
+
toast.success("Deleted successfully!");
|
|
154
|
+
router.push("/manager");
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
toast.error("Erro ao deletar recurso");
|
|
158
|
+
router.push("/manager");
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
} catch (error) {
|
|
162
|
+
console.log(error);
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
const onCancel = () => {
|
|
167
|
+
if (routerLinkCancel) {
|
|
168
|
+
router.push(routerLinkCancel);
|
|
169
|
+
} else {
|
|
170
|
+
router.back();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return (
|
|
175
|
+
<form onSubmit={handleSubmit} className="flex justify-center gap-4 py-3">
|
|
176
|
+
<button type="button" onClick={onCancel} className="px-4 py-2 rounded bg-gray-300">
|
|
177
|
+
Cancelar
|
|
178
|
+
</button>
|
|
179
|
+
|
|
180
|
+
<button type="submit" className="px-4 py-2 rounded bg-red-600 text-white">
|
|
181
|
+
Confirmar e Deletar
|
|
182
|
+
</button>
|
|
183
|
+
</form>
|
|
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 { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
198
|
+
import { formSchema, FormSchemaType } from "./form-scheme";
|
|
199
|
+
import { InputCustom } from "@/components/Inputs/InputCustom";
|
|
200
|
+
import { InputRichTextEditor } from "@/components/Inputs/InputRichTextEditor";
|
|
201
|
+
import { delay } from "@/utils/utils";
|
|
202
|
+
|
|
203
|
+
type FormEdit${resourceInSingular}Props = { ${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular}; };
|
|
204
|
+
export const FormEdit${resourceInSingular}: React.FC<FormEdit${resourceInSingular}Props> = ({ ${resourceInSingular.toLocaleLowerCase()} }) => {
|
|
205
|
+
const router = useRouter();
|
|
206
|
+
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|
207
|
+
const [photoPreview, setPhotoPreview] = useState<string | null>(${resourceInSingular.toLocaleLowerCase()}.image ? \`\${process.env.NEXT_PUBLIC_BACKEND_URL}\${${resourceInSingular.toLocaleLowerCase()}.image}\` : null);
|
|
208
|
+
const methods = useForm<FormSchemaType>({
|
|
209
|
+
resolver: yupResolver(formSchema), mode: "onChange",
|
|
210
|
+
defaultValues: { title: ${resourceInSingular.toLocaleLowerCase()}.title, content: ${resourceInSingular.toLocaleLowerCase()}.content, status: ${resourceInSingular.toLocaleLowerCase()}.status },
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
const { handleSubmit, watch, formState: { isSubmitting } } = methods;
|
|
214
|
+
const titlePreview = watch("title");
|
|
215
|
+
const onSubmit = async (data: FormSchemaType) => {
|
|
216
|
+
const payload = Object.fromEntries(Object.entries(data).filter(([_, value]) => value !== undefined));
|
|
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.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id}\`, {
|
|
224
|
+
method: "POST",
|
|
225
|
+
headers: { "Content-Type": "application/json" },
|
|
226
|
+
body: JSON.stringify(payload),
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
if (!response.ok) { toast.error("Erro ao atualizar o ${resourceInSingular.toLocaleLowerCase()}"); return; };
|
|
230
|
+
|
|
231
|
+
if (photoFile) {
|
|
232
|
+
const formData = new FormData();
|
|
233
|
+
formData.append("photo", photoFile, photoFile.name);
|
|
234
|
+
await delay(3000);
|
|
235
|
+
const uploadResp = await fetch(\`\${process.env.NEXT_PUBLIC_FRONTEND_URL}/api/images/${resourceInSingular.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id}\`, { method: "POST", body: formData });
|
|
236
|
+
if (!uploadResp.ok) {
|
|
237
|
+
toast.warning("os dados do ${resourceInSingular.toLocaleLowerCase()} foram salvos, mas a imagem n\xE3o foi possivel carregar e atualizar, tente novamente...");
|
|
238
|
+
router.refresh();
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
toast.success("${resourceInSingular} atualizado com sucesso!");
|
|
244
|
+
router.push("/manager")
|
|
245
|
+
} catch {
|
|
246
|
+
toast.error("Erro ao comunicar com o servidor");
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
251
|
+
const file = e.target.files?.[0] ?? null;
|
|
252
|
+
if (photoPreview?.startsWith("blob:")) {
|
|
253
|
+
URL.revokeObjectURL(photoPreview);
|
|
254
|
+
}
|
|
255
|
+
setPhotoFile(file);
|
|
256
|
+
if (file) {
|
|
257
|
+
setPhotoPreview(URL.createObjectURL(file));
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
useEffect(() => {
|
|
262
|
+
return () => {
|
|
263
|
+
if (photoPreview?.startsWith("blob:")) URL.revokeObjectURL(photoPreview);
|
|
264
|
+
};
|
|
265
|
+
}, [photoPreview]);
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<FormProvider {...methods}>
|
|
269
|
+
<form onSubmit={handleSubmit(onSubmit)} className="max-w-6xl mx-auto px-6 py-4 flex flex-col gap-6">
|
|
270
|
+
<h1 className="text-3xl font-bold">
|
|
271
|
+
{titlePreview || "Pr\xE9via do t\xEDtulo do ${resourceInSingular}"}
|
|
272
|
+
</h1>
|
|
273
|
+
<InputCustom name="title" label="T\xEDtulo do ${resourceInSingular}" />
|
|
274
|
+
{photoPreview && (<img src={photoPreview} className="h-64 w-full object-cover rounded-lg" alt="Pr\xE9via" />)}
|
|
275
|
+
|
|
276
|
+
<input type="file" accept="image/*" onChange={handleFileChange} />
|
|
277
|
+
<InputRichTextEditor name="content" label="Conte\xFAdo" placeholder="Atualize o conte\xFAdo do ${resourceInSingular}..." />
|
|
278
|
+
|
|
279
|
+
<button type="submit" disabled={isSubmitting} className="bg-blue-700 text-white px-6 py-2 rounded-md disabled:opacity-50"> Salvar
|
|
280
|
+
</button>
|
|
281
|
+
</form>
|
|
282
|
+
</FormProvider>
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
`;
|
|
287
|
+
|
|
288
|
+
// src/templates/forms/schems/update-form-scheme.ts
|
|
289
|
+
var formSchemeUpdateTemplate = (resourceInSingular, resourceInPlural) => `
|
|
290
|
+
import * as yup from "yup";
|
|
291
|
+
import { ${resourceInSingular}Status } from "@/utils/models/${resourceInSingular.toLocaleLowerCase()}";
|
|
292
|
+
// ${resourceInPlural} se tiver campo "imagem" lembrar nao incluir, uploadde imagem pesada deve ser feita separadamente
|
|
293
|
+
|
|
294
|
+
export const formSchema = yup.object({
|
|
295
|
+
title: yup.string().transform((value) => (value === "" ? undefined : value)).min(3, "O t\xEDtulo deve ter pelo menos 3 caracteres").max(150, "O t\xEDtulo deve ter no m\xE1ximo 150 caracteres").nullable().defined(),
|
|
296
|
+
content: yup.string().transform((value) => (value === "" ? undefined : value)).min(10, "O conte\xFAdo deve ter pelo menos 10 caracteres").nullable().defined(),
|
|
297
|
+
status: yup.mixed<${resourceInSingular}Status>().oneOf(Object.values(${resourceInSingular}Status)).nullable().defined(),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
export type FormSchemaType = yup.Asserts<typeof formSchema>;
|
|
301
|
+
`;
|
|
302
|
+
|
|
303
|
+
// src/templates/pages/detail-page.ts
|
|
304
|
+
var detailPageTemplate = (resourceInSingular) => `
|
|
305
|
+
interface PageProps {params: {${resourceInSingular.toLocaleLowerCase()}Id: string;};};
|
|
306
|
+
|
|
307
|
+
export default function ${resourceInSingular}DetailPage({ params }: PageProps) {
|
|
308
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = params;
|
|
309
|
+
return (
|
|
310
|
+
<div className="w-full min-h-screen flex flex-col bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
311
|
+
${resourceInSingular} detail: {${resourceInSingular.toLocaleLowerCase()}Id}
|
|
312
|
+
</div>
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
`;
|
|
316
|
+
|
|
317
|
+
// src/templates/pages/list-page.ts
|
|
318
|
+
var listPageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
319
|
+
// import { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
320
|
+
import Link from "next/link";
|
|
321
|
+
import { FileCogIcon, FileText } from "lucide-react";
|
|
322
|
+
|
|
323
|
+
type ${resourceInSingular} = {};
|
|
324
|
+
async function get${resourceInPlural.toLocaleLowerCase()}(): Promise<${resourceInSingular}[]> {
|
|
325
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/${resourceInPlural.toLowerCase()}\`, {
|
|
326
|
+
cache: "no-store",
|
|
327
|
+
});
|
|
328
|
+
if (!response.ok) return [];
|
|
329
|
+
const ${resourceInPlural.toLocaleLowerCase()}: ${resourceInSingular}[] = await response.json();
|
|
330
|
+
return ${resourceInPlural.toLocaleLowerCase()};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export default async function ${resourceInPlural}Page() {
|
|
334
|
+
const ${resourceInPlural.toLocaleLowerCase()}: ${resourceInSingular}[] = await get${resourceInPlural.toLocaleLowerCase()}();
|
|
335
|
+
|
|
336
|
+
return (
|
|
337
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
338
|
+
<div className="max-w-6xl mx-auto">
|
|
339
|
+
{/* Header */}
|
|
340
|
+
<header className="flex items-center justify-between mb-10">
|
|
341
|
+
<div className="flex items-center gap-3">
|
|
342
|
+
<div className="p-2 rounded-xl bg-primary/10 text-primary">
|
|
343
|
+
<FileText className="w-6 h-6" />
|
|
344
|
+
</div>
|
|
345
|
+
<div>
|
|
346
|
+
<h1 className="text-3xl font-semibold">${resourceInPlural}</h1>
|
|
347
|
+
<p className="text-muted-foreground">
|
|
348
|
+
Lista de ${resourceInPlural.toLocaleLowerCase()} publicados na aplica\xE7\xE3o
|
|
349
|
+
</p>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
</header>
|
|
353
|
+
|
|
354
|
+
{/* ${resourceInPlural} list */}
|
|
355
|
+
<section className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
356
|
+
{${resourceInPlural.toLocaleLowerCase().toLocaleLowerCase()}.map((${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular}) => (
|
|
357
|
+
<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">
|
|
358
|
+
<div className="flex items-start gap-3">
|
|
359
|
+
<div className="p-2 w-full bg-muted text-muted-foreground ">
|
|
360
|
+
<h2 className="font-semibold text-lg leading-snug">
|
|
361
|
+
{${resourceInSingular.toLocaleLowerCase()}.title}
|
|
362
|
+
</h2>
|
|
363
|
+
<div className="flex items-center justify-between">
|
|
364
|
+
<FileText className="w-5 h-5" />
|
|
365
|
+
<Link href={\`/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLowerCase()}Id}/config\`} className="cursor-pointer p-2 hover:bg-amber-400 rounded-2xl">
|
|
366
|
+
<FileCogIcon className="w-5 h-5" />
|
|
367
|
+
</Link>
|
|
368
|
+
</div>
|
|
369
|
+
|
|
370
|
+
</div>
|
|
371
|
+
</div>
|
|
372
|
+
<div className="flex items-center justify-around">
|
|
373
|
+
<Link href={\`/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id}/edit\`} >editar</Link>
|
|
374
|
+
<Link href={\`/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}.${resourceInSingular.toLocaleLowerCase()}Id}/delete\`} >excluir</Link>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
</article>
|
|
378
|
+
))}
|
|
379
|
+
</section>
|
|
380
|
+
|
|
381
|
+
{/* Empty state */}
|
|
382
|
+
{${resourceInPlural.toLocaleLowerCase()}.length === 0 && (
|
|
383
|
+
<div className="mt-20 text-center text-muted-foreground">
|
|
384
|
+
<FileText className="w-10 h-10 mx-auto mb-3 opacity-50" />
|
|
385
|
+
<p>Nenhum ${resourceInSingular.toLocaleLowerCase()} encontrado</p>
|
|
386
|
+
</div>
|
|
387
|
+
)}
|
|
388
|
+
</div>
|
|
389
|
+
</div>
|
|
390
|
+
);
|
|
391
|
+
};
|
|
392
|
+
`;
|
|
393
|
+
|
|
394
|
+
// src/templates/pages/new-page.ts
|
|
395
|
+
var newPageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
396
|
+
import { FilePlus2, Info } from "lucide-react";
|
|
397
|
+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
398
|
+
import { Session } from "@/utils/route";
|
|
399
|
+
import { getServerSession } from "next-auth";
|
|
400
|
+
import { redirect } from "next/navigation";
|
|
401
|
+
import { FormNew${resourceInSingular} } from "@/forms/${resourceInPlural.toLocaleLowerCase()}";
|
|
402
|
+
|
|
403
|
+
export default async function New${resourceInSingular}Page() {
|
|
404
|
+
const session: Session | null = await getServerSession(authOptions);
|
|
405
|
+
if (!session) redirect("/");
|
|
406
|
+
|
|
407
|
+
return (
|
|
408
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] transition-colors duration-500">
|
|
409
|
+
<div className="max-w-4xl mx-auto px-6 py-12">
|
|
410
|
+
<header className="mb-10">
|
|
411
|
+
<div className="flex items-start gap-4">
|
|
412
|
+
<div className="p-3 rounded-2xl bg-primary/10 text-primary">
|
|
413
|
+
<FilePlus2 className="w-6 h-6" />
|
|
414
|
+
</div>
|
|
415
|
+
<div>
|
|
416
|
+
<h1 className="text-3xl font-semibold">Criar novo post</h1>
|
|
417
|
+
<p className="mt-1 text-muted-foreground max-w-2xl">
|
|
418
|
+
Preencha as informa\xE7\xF5es abaixo para publicar um novo post no blog.
|
|
419
|
+
Capriche no t\xEDtulo e no conte\xFAdo para melhorar a leitura e o
|
|
420
|
+
engajamento.
|
|
421
|
+
</p>
|
|
422
|
+
</div>
|
|
423
|
+
</div>
|
|
424
|
+
</header>
|
|
425
|
+
|
|
426
|
+
{/* Info helper */}
|
|
427
|
+
<section className="mb-12 rounded-2xl bg-background shadow-sm p-6 flex gap-3">
|
|
428
|
+
<Info className="w-5 h-5 text-muted-foreground mt-0.5" />
|
|
429
|
+
<p className="text-sm text-muted-foreground">
|
|
430
|
+
Voc\xEA pode editar este ${resourceInSingular.toLowerCase()} depois de publicado. Certifique-se de revisar
|
|
431
|
+
o conte\xFAdo antes de salvar.
|
|
432
|
+
</p>
|
|
433
|
+
</section>
|
|
434
|
+
|
|
435
|
+
<FormNew${resourceInSingular} />
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
`;
|
|
441
|
+
|
|
442
|
+
// src/templates/pages/manager-page.ts
|
|
443
|
+
var managerPageTemplate = () => `
|
|
444
|
+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
445
|
+
import { Session } from "@/utils/route";
|
|
446
|
+
import { getServerSession } from "next-auth";
|
|
447
|
+
import { redirect } from "next/navigation";
|
|
448
|
+
|
|
449
|
+
export default async function Manager() {
|
|
450
|
+
const session: Session | null = await getServerSession(authOptions);
|
|
451
|
+
if (!session) redirect("/");
|
|
452
|
+
|
|
453
|
+
return (
|
|
454
|
+
<div className="w-full min-h-screen relative bg-(--bg-section-100) transition-colors duration-500 p-6">
|
|
455
|
+
<h2>Bora major.catuca nesse freelas..</h2>
|
|
456
|
+
</div >
|
|
457
|
+
);
|
|
458
|
+
};
|
|
459
|
+
`;
|
|
460
|
+
|
|
461
|
+
// src/templates/routes/manager-context.ts
|
|
462
|
+
var managerContextTemplate = () => `
|
|
463
|
+
"use client";
|
|
464
|
+
|
|
465
|
+
import { createContext, useContext, useState, ReactNode } from "react";
|
|
466
|
+
|
|
467
|
+
type MenuContextType = {
|
|
468
|
+
menuActive: boolean;
|
|
469
|
+
setMenuActive: (value: boolean) => void;
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const MenuContext = createContext<MenuContextType | undefined>(undefined);
|
|
473
|
+
|
|
474
|
+
export const MenuProvider = ({ children }: { children: ReactNode }) => {
|
|
475
|
+
const [menuActive, setMenuActive] = useState(true);
|
|
476
|
+
return (
|
|
477
|
+
<MenuContext.Provider value={{ menuActive, setMenuActive }}>
|
|
478
|
+
{children}
|
|
479
|
+
</MenuContext.Provider>
|
|
480
|
+
);
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
export const useMenu = () => {
|
|
484
|
+
const context = useContext(MenuContext);
|
|
485
|
+
if (!context) throw new Error("useMenu must be used within MenuProvider");
|
|
486
|
+
return context;
|
|
487
|
+
};
|
|
488
|
+
`;
|
|
489
|
+
|
|
490
|
+
// src/templates/inputs/input-template.ts
|
|
491
|
+
var inputTemplate = () => `
|
|
492
|
+
"use client";
|
|
493
|
+
import { useFormContext, Controller } from 'react-hook-form';
|
|
494
|
+
|
|
495
|
+
interface InputProps {
|
|
496
|
+
name: string;
|
|
497
|
+
label: string;
|
|
498
|
+
type?: string;
|
|
499
|
+
required?: boolean;
|
|
500
|
+
ishidden?: boolean;
|
|
501
|
+
pattern?: RegExp;
|
|
502
|
+
placeholder?: string;
|
|
503
|
+
asDate?: boolean;
|
|
504
|
+
multiline?: boolean;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
export const InputCustom: React.FC<InputProps> = ({ name, label, type = "text", required, pattern, placeholder, asDate, ishidden, multiline = false }) => {
|
|
508
|
+
const { control, formState: { errors } } = useFormContext();
|
|
509
|
+
|
|
510
|
+
return (
|
|
511
|
+
<div className="flex flex-col gap-2">
|
|
512
|
+
{!ishidden && (
|
|
513
|
+
<label htmlFor={name} className="text-sm font-medium text-[--text-label]">
|
|
514
|
+
{label}:
|
|
515
|
+
</label>
|
|
516
|
+
)}
|
|
517
|
+
|
|
518
|
+
<Controller
|
|
519
|
+
control={control}
|
|
520
|
+
name={name}
|
|
521
|
+
rules={{
|
|
522
|
+
required: !ishidden && required ? "Campo obrigat\xF3rio" : false,
|
|
523
|
+
pattern: pattern ? { value: pattern, message: "Formato inv\xE1lido" } : undefined,
|
|
524
|
+
}}
|
|
525
|
+
render={({ field }) => {
|
|
526
|
+
const value = asDate && typeof field.value === "string" ? field.value.split("T")[0] : field.value ?? "";
|
|
527
|
+
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";
|
|
528
|
+
const errorClass = errors[name] ? "border-red-400" : "border-gray-300";
|
|
529
|
+
|
|
530
|
+
if (multiline) {
|
|
531
|
+
return (
|
|
532
|
+
<textarea {...field} id={name} rows={5} placeholder={placeholder} value={value} onChange={field.onChange} className={\`\${baseClasses} \${errorClass} rounded-xl resize-y min-h-30\`}/>
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
return (
|
|
537
|
+
<input {...field} id={name} type={type} placeholder={ishidden ? "" : placeholder} hidden={ishidden} aria-hidden={ishidden} value={value} onChange={(e) => {
|
|
538
|
+
if (asDate) {
|
|
539
|
+
const v = e.target.value;
|
|
540
|
+
if (!v) {
|
|
541
|
+
field.onChange(null);
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const date = new Date(v);
|
|
545
|
+
if (isNaN(date.getTime())) return;
|
|
546
|
+
|
|
547
|
+
field.onChange(date.toISOString());
|
|
548
|
+
} else {
|
|
549
|
+
field.onChange(e);
|
|
550
|
+
}
|
|
551
|
+
}}
|
|
552
|
+
className={\`\${baseClasses} \${errorClass} rounded-full\`}
|
|
553
|
+
/>
|
|
554
|
+
);
|
|
555
|
+
}}
|
|
556
|
+
/>
|
|
557
|
+
{errors[name]?.message && (
|
|
558
|
+
<p className="text-sm text-red-500">
|
|
559
|
+
{errors[name]?.message as string}
|
|
560
|
+
</p>
|
|
561
|
+
)}
|
|
562
|
+
</div>
|
|
563
|
+
);
|
|
564
|
+
};
|
|
565
|
+
`;
|
|
566
|
+
var inputFormHelperTestUnitTemplate = () => `
|
|
567
|
+
import { ReactNode } from "react";
|
|
568
|
+
import { render } from "@testing-library/react";
|
|
569
|
+
import { useForm, FormProvider } from "react-hook-form";
|
|
570
|
+
|
|
571
|
+
export function renderWithForm(ui: ReactNode) {
|
|
572
|
+
const Wrapper = ({ children }: { children: ReactNode }) => {
|
|
573
|
+
const methods = useForm();
|
|
574
|
+
|
|
575
|
+
return (
|
|
576
|
+
<form onSubmit={methods.handleSubmit(() => { })}>
|
|
577
|
+
<FormProvider {...methods}>
|
|
578
|
+
{children}
|
|
579
|
+
<button type="submit">submit</button>
|
|
580
|
+
</FormProvider>
|
|
581
|
+
</form>
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
return render(ui, { wrapper: Wrapper });
|
|
587
|
+
}`;
|
|
588
|
+
var inputTestUnitTemplate = () => `
|
|
589
|
+
import { screen, fireEvent } from "@testing-library/react";
|
|
590
|
+
import { InputCustom } from ".";
|
|
591
|
+
import { renderWithForm } from "./renderWithForm";
|
|
592
|
+
|
|
593
|
+
describe("InputCustom component", () => {
|
|
594
|
+
|
|
595
|
+
it("should render the label and the input.", () => {
|
|
596
|
+
renderWithForm(<InputCustom name="email" label="Email" placeholder="Digite o email" />);
|
|
597
|
+
|
|
598
|
+
expect(screen.getByLabelText("Email:")).toBeInTheDocument();
|
|
599
|
+
expect(screen.getByPlaceholderText("Digite o email")).toBeInTheDocument();
|
|
600
|
+
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
it("should allows you to type in the field", () => {
|
|
604
|
+
renderWithForm(<InputCustom name="name" label="Nome" placeholder="Digite seu nome" />);
|
|
605
|
+
|
|
606
|
+
const input = screen.getByPlaceholderText("Digite seu nome");
|
|
607
|
+
|
|
608
|
+
fireEvent.change(input, { target: { value: "Jo\xE3o" } });
|
|
609
|
+
|
|
610
|
+
expect(input).toHaveValue("Jo\xE3o");
|
|
611
|
+
|
|
612
|
+
});
|
|
613
|
+
|
|
614
|
+
it("should shows error when required.", async () => {
|
|
615
|
+
renderWithForm(<InputCustom name="name" label="Nome" required />);
|
|
616
|
+
|
|
617
|
+
fireEvent.click(screen.getByText("submit"));
|
|
618
|
+
|
|
619
|
+
expect(await screen.findByText("Campo obrigat\xF3rio")).toBeInTheDocument();
|
|
620
|
+
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
it("should validate pattern", async () => {
|
|
624
|
+
renderWithForm(<InputCustom name="cpf" label="CPF" pattern={/^d{3}$/} placeholder="123" />);
|
|
625
|
+
|
|
626
|
+
const input = screen.getByPlaceholderText("123");
|
|
627
|
+
|
|
628
|
+
fireEvent.change(input, { target: { value: "abc" } });
|
|
629
|
+
fireEvent.click(screen.getByText("submit"));
|
|
630
|
+
|
|
631
|
+
expect(await screen.findByText("Formato inv\xE1lido")).toBeInTheDocument();
|
|
632
|
+
|
|
633
|
+
});
|
|
634
|
+
|
|
635
|
+
it("should renders textarea", () => {
|
|
636
|
+
renderWithForm(<InputCustom name="bio" label="Bio" multiline />);
|
|
637
|
+
|
|
638
|
+
const textarea = screen.getByRole("textbox");
|
|
639
|
+
expect(textarea.tagName).toBe("TEXTAREA");
|
|
640
|
+
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
});
|
|
644
|
+
|
|
645
|
+
`;
|
|
646
|
+
|
|
647
|
+
// src/templates/routes/nextauth-route.ts
|
|
648
|
+
var nextConfigTemplate = () => `
|
|
649
|
+
import NextAuth, { NextAuthOptions } from "next-auth";
|
|
650
|
+
import CredentialsProvider from "next-auth/providers/credentials";
|
|
651
|
+
import { cookies } from "next/headers";
|
|
652
|
+
import { IAuthenticateUser, requestAuthenticationUser } from "../request-api";
|
|
653
|
+
import { decoderTokenToClaims } from "../decode-claims";
|
|
654
|
+
|
|
655
|
+
export type User = { id: string; name: string; email: string; image: string; userStatus?: string; accessToken: string; };
|
|
656
|
+
|
|
657
|
+
export const authOptions: NextAuthOptions = {
|
|
658
|
+
pages: {
|
|
659
|
+
signIn: "/",
|
|
660
|
+
},
|
|
661
|
+
providers: [
|
|
662
|
+
CredentialsProvider({
|
|
663
|
+
name: "Credentials",
|
|
664
|
+
credentials: {
|
|
665
|
+
email: { label: "Email", type: "email", placeholder: "example@gmail.com" },
|
|
666
|
+
password: { label: "Password", type: "password" },
|
|
667
|
+
},
|
|
668
|
+
async authorize(credentials, req): Promise<User | null> {
|
|
669
|
+
const user: IAuthenticateUser = {
|
|
670
|
+
email: credentials?.email,
|
|
671
|
+
password: credentials?.password,
|
|
672
|
+
clientType: "web",
|
|
673
|
+
};
|
|
674
|
+
const resposta = await requestAuthenticationUser(user);
|
|
675
|
+
|
|
676
|
+
if (resposta.accessToken && resposta.accessToken !== "User with credentials not found or invalid") {
|
|
677
|
+
(await cookies()).set("jwt_back", resposta.accessToken, { httpOnly: true, sameSite: "lax", path: "/", maxAge: 3600 });
|
|
678
|
+
(await cookies()).set("jwt_back_refresh", resposta.refreshToken, { httpOnly: true, sameSite: "lax", path: "/", maxAge: 3600 });
|
|
679
|
+
|
|
680
|
+
const decodedClaims = decoderTokenToClaims(resposta.accessToken);
|
|
681
|
+
return { id: decodedClaims?.id ?? "", name: decodedClaims?.username ?? "", email: decodedClaims?.email ?? "", image: decodedClaims?.imagem ?? "", userStatus: decodedClaims?.userStatus ?? "unknown", accessToken: resposta.accessToken } as User;
|
|
682
|
+
}
|
|
683
|
+
return null;
|
|
684
|
+
},
|
|
685
|
+
}),
|
|
686
|
+
],
|
|
687
|
+
|
|
688
|
+
callbacks: {
|
|
689
|
+
async jwt({ token, user, trigger, session }) {
|
|
690
|
+
if (user) {
|
|
691
|
+
const u = user as any;
|
|
692
|
+
token.id = u.id ?? "";
|
|
693
|
+
token.name = u.name ?? "";
|
|
694
|
+
token.email = u.email ?? "";
|
|
695
|
+
token.image = u.image ?? "";
|
|
696
|
+
token.userStatus = u.userStatus ?? "";
|
|
697
|
+
if ("accessToken" in u) token.accessToken = u.accessToken ?? "";
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (trigger === "update" && session?.user) {
|
|
701
|
+
token.name = session.user.name ?? token.name;
|
|
702
|
+
token.email = session.user.email ?? token.email;
|
|
703
|
+
token.image = session.user.image ?? token.image;
|
|
704
|
+
token.userStatus = session.user.userStatus ?? token.userStatus;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
return token;
|
|
708
|
+
},
|
|
709
|
+
async session({ session, token }) {
|
|
710
|
+
if (session.user) {
|
|
711
|
+
(session.user as any).id = (token as any).id;
|
|
712
|
+
session.user.name = (token as any).name;
|
|
713
|
+
session.user.email = (token as any).email;
|
|
714
|
+
const image = typeof (token as any).image === "string" ? (token as any).image : undefined;
|
|
715
|
+
session.user.image = image;
|
|
716
|
+
(session.user as any).userStatus = (token as any).userStatus;
|
|
717
|
+
}
|
|
718
|
+
(session as any).accessToken = (token as any).accessToken ?? null;
|
|
719
|
+
return session;
|
|
720
|
+
},
|
|
721
|
+
},
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
const handler = NextAuth(authOptions);
|
|
725
|
+
export { handler as GET, handler as POST };
|
|
726
|
+
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
// src/templates/config/decode-claims.ts
|
|
730
|
+
var nextDecodeClaimsTemplate = () => `
|
|
731
|
+
import { jwtDecode } from 'jwt-decode';
|
|
732
|
+
|
|
733
|
+
export interface IObjectClaims { id: string; username: string; email: string; imagem: string; roles: string | string[]; userStatus?: string; };
|
|
734
|
+
|
|
735
|
+
export const decoderTokenToClaims = (token: string): Partial<IObjectClaims> | null => {
|
|
736
|
+
if (token) {
|
|
737
|
+
try {
|
|
738
|
+
const decodedToken = jwtDecode(token) as any;
|
|
739
|
+
return { id: decodedToken.userId, username: decodedToken.userName, email: decodedToken.email, imagem: decodedToken.image, roles: decodedToken.roles, userStatus: decodedToken.userStatus };
|
|
740
|
+
} catch (error) {
|
|
741
|
+
return null;
|
|
742
|
+
}
|
|
743
|
+
} else {
|
|
744
|
+
return null;
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
`;
|
|
748
|
+
|
|
749
|
+
// src/templates/config/request-api.ts
|
|
750
|
+
var nextRequestApiTemplate = () => `
|
|
751
|
+
export interface IAuthenticateUser { email: string | undefined; password: string | undefined; clientType: string; };
|
|
752
|
+
|
|
753
|
+
export const requestAuthenticationUser = async (user: IAuthenticateUser) => {
|
|
754
|
+
const resposta = await fetch(\`\${process.env.NEXT_BACKEND_URL}/auth/login\`, {
|
|
755
|
+
method: 'POST',
|
|
756
|
+
headers: {
|
|
757
|
+
"Content-Type": "application/json"
|
|
758
|
+
},
|
|
759
|
+
body: JSON.stringify(user)
|
|
760
|
+
});
|
|
761
|
+
if (resposta.ok) return await resposta.json();
|
|
762
|
+
return null;
|
|
763
|
+
|
|
764
|
+
};
|
|
765
|
+
`;
|
|
766
|
+
|
|
767
|
+
// src/templates/config/session.ts
|
|
768
|
+
var nextSessionTypeTemplate = () => `
|
|
769
|
+
export interface Session {
|
|
770
|
+
user: { name: string; email: string; image: string; id: string; userStatus: string; };
|
|
771
|
+
accessToken: string;
|
|
772
|
+
}
|
|
773
|
+
`;
|
|
774
|
+
|
|
775
|
+
// src/templates/config/utils.ts
|
|
776
|
+
var utilsTypeTemplate = () => `export const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));`;
|
|
777
|
+
|
|
778
|
+
// src/templates/config/environment.ts
|
|
779
|
+
var environmentTemplate = () => `
|
|
780
|
+
NEXTAUTH_URL=http://localhost:3000
|
|
781
|
+
NEXTAUTH_SECRET=secret_key
|
|
782
|
+
|
|
783
|
+
NEXT_FRONTEND_URL=http://localhost:3000
|
|
784
|
+
NEXT_PUBLIC_FRONTEND_URL=http://localhost:3000
|
|
785
|
+
|
|
786
|
+
NEXT_BACKEND_URL=http://localhost:8000
|
|
787
|
+
NEXT_PUBLIC_BACKEND_URL=http://localhost:8000
|
|
788
|
+
|
|
789
|
+
`;
|
|
790
|
+
|
|
791
|
+
// src/templates/routes/image-upload-route.ts
|
|
792
|
+
var imageUploadTemplate = () => `
|
|
793
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
794
|
+
import { cookies } from "next/headers";
|
|
795
|
+
|
|
796
|
+
export async function POST(req: NextRequest, context: { params: Promise<{ resource: string; resourceId: string }> }) {
|
|
797
|
+
const { resource, resourceId } = await context.params;
|
|
798
|
+
|
|
799
|
+
try {
|
|
800
|
+
const cookieStore = cookies();
|
|
801
|
+
const token = (await cookieStore).get("jwt_back");
|
|
802
|
+
const jwt = token?.value ?? "not found";
|
|
803
|
+
const backendForm = new FormData();
|
|
804
|
+
const formData = await req.formData();
|
|
805
|
+
const photo = formData.get("photo");
|
|
806
|
+
|
|
807
|
+
if (photo && photo instanceof Blob) {
|
|
808
|
+
backendForm.append("file", photo);
|
|
809
|
+
} else {
|
|
810
|
+
return NextResponse.json({ message: "Nenhuma foto enviada" }, { status: 400 });
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
const init: RequestInit & { duplex?: 'half' } = {
|
|
814
|
+
method: "PATCH",
|
|
815
|
+
headers: {
|
|
816
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
817
|
+
},
|
|
818
|
+
body: backendForm,
|
|
819
|
+
duplex: "half",
|
|
820
|
+
};
|
|
821
|
+
|
|
822
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/\${resource}/\${resourceId}/photo\`, init);
|
|
823
|
+
const data = await response.json();
|
|
824
|
+
return NextResponse.json(data, { status: response.status });
|
|
825
|
+
|
|
826
|
+
} catch (err) {
|
|
827
|
+
console.error("Erro no route handler:", err);
|
|
828
|
+
return NextResponse.json({ message: "Erro ao conectar no backend" }, { status: 500 });
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
`;
|
|
832
|
+
|
|
833
|
+
// src/templates/routes/delete-route.ts
|
|
834
|
+
var deleteTemplate = () => `
|
|
835
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
836
|
+
import { cookies } from "next/headers";
|
|
837
|
+
|
|
838
|
+
export async function POST(req: NextRequest, context: { params: Promise<{ resource: string; resourceId: string }> }) {
|
|
839
|
+
const { resource, resourceId } = await context.params;
|
|
840
|
+
|
|
841
|
+
try {
|
|
842
|
+
const cookieStore = cookies();
|
|
843
|
+
const token = (await cookieStore).get("jwt_back");
|
|
844
|
+
const jwt = token?.value ?? "not found";
|
|
845
|
+
|
|
846
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/\${resource}/\${resourceId}\`, {
|
|
847
|
+
method: "DELETE",
|
|
848
|
+
headers: {
|
|
849
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
850
|
+
},
|
|
851
|
+
});
|
|
852
|
+
console.log("Response status from backend:", response.status);
|
|
853
|
+
return NextResponse.json({ status: response.status });
|
|
854
|
+
|
|
855
|
+
} catch (err) {
|
|
856
|
+
console.error("Erro no route handler:", err);
|
|
857
|
+
return NextResponse.json({ message: "Erro ao conectar no backend" }, { status: 500 });
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
`;
|
|
862
|
+
|
|
863
|
+
// src/templates/routes/set-user-photo-route.ts
|
|
864
|
+
var usersPhotoTemplate = () => `
|
|
865
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
866
|
+
import { cookies } from "next/headers";
|
|
867
|
+
import { decoderTokenToClaims } from "../../auth/decode-claims";
|
|
868
|
+
|
|
869
|
+
export async function POST(req: NextRequest) {
|
|
870
|
+
try {
|
|
871
|
+
const cookieStore = cookies();
|
|
872
|
+
const token = (await cookieStore).get("jwt_back");
|
|
873
|
+
const jwt = token?.value ?? "not found";
|
|
874
|
+
const backendForm = new FormData();
|
|
875
|
+
|
|
876
|
+
const formData = await req.formData();
|
|
877
|
+
const photo = formData.get("photo");
|
|
878
|
+
|
|
879
|
+
if (photo && photo instanceof Blob) {
|
|
880
|
+
backendForm.append("photo", photo);
|
|
881
|
+
} else {
|
|
882
|
+
return NextResponse.json({ message: "Nenhuma foto enviada" }, { status: 400 });
|
|
883
|
+
}
|
|
884
|
+
const user = decoderTokenToClaims(jwt);
|
|
885
|
+
const init: RequestInit & { duplex?: 'half' } = {
|
|
886
|
+
method: "PATCH",
|
|
887
|
+
headers: {
|
|
888
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
889
|
+
},
|
|
890
|
+
body: backendForm,
|
|
891
|
+
duplex: "half",
|
|
892
|
+
};
|
|
893
|
+
|
|
894
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/users/\${user?.id}/photo\`, init);
|
|
895
|
+
const data = await response.json();
|
|
896
|
+
return NextResponse.json(data, { status: response.status });
|
|
897
|
+
|
|
898
|
+
} catch (err) {
|
|
899
|
+
console.error("Erro no route handler:", err);
|
|
900
|
+
return NextResponse.json({ message: "Erro ao conectar no backend" }, { status: 500 });
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
`;
|
|
904
|
+
|
|
905
|
+
// src/templates/routes/user-create-route.ts
|
|
906
|
+
var usersCreateTemplate = () => `
|
|
907
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
908
|
+
|
|
909
|
+
export async function POST(req: NextRequest): Promise<NextResponse> {
|
|
910
|
+
try {
|
|
911
|
+
const body = await req.json();
|
|
912
|
+
|
|
913
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/users\`, {
|
|
914
|
+
method: 'POST',
|
|
915
|
+
headers: {
|
|
916
|
+
'Content-Type': 'application/json',
|
|
917
|
+
},
|
|
918
|
+
body: JSON.stringify(body),
|
|
919
|
+
});
|
|
920
|
+
const contentType = response.headers.get("Content-Type");
|
|
921
|
+
const usuario = await response.json();
|
|
922
|
+
console.log(contentType);
|
|
923
|
+
console.log(JSON.stringify(usuario));
|
|
924
|
+
|
|
925
|
+
if (response.status === 201) {
|
|
926
|
+
return NextResponse.json(usuario, { status: 201 });
|
|
927
|
+
}
|
|
928
|
+
console.log(response.status);
|
|
929
|
+
return NextResponse.json({ message: "The request contains errors, such as invalid data, incorrect format, or missing required fields." }, { status: 400 });
|
|
930
|
+
|
|
931
|
+
} catch (error) {
|
|
932
|
+
throw new Error("Erro ao conectar no backend.");
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
`;
|
|
936
|
+
|
|
937
|
+
// src/templates/routes/user-update-route.ts
|
|
938
|
+
var usersUpdateTemplate = () => `
|
|
939
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
940
|
+
import { cookies } from "next/headers";
|
|
941
|
+
import { decoderTokenToClaims } from "../../auth/decode-claims";
|
|
942
|
+
|
|
943
|
+
export async function POST(req: NextRequest): Promise<NextResponse> {
|
|
944
|
+
try {
|
|
945
|
+
const cookieStore = cookies();
|
|
946
|
+
const token = (await cookieStore).get("jwt_back");
|
|
947
|
+
let jwt = !token ? "not found" : token.value;
|
|
948
|
+
|
|
949
|
+
const refreshtoken = (await cookieStore).get("jwt_back_refresh");
|
|
950
|
+
let refreshJwt = !refreshtoken ? "not found" : refreshtoken.value;
|
|
951
|
+
if (!jwt || !refreshJwt)
|
|
952
|
+
return NextResponse.json({ message: "Token n\xE3o encontrado." }, { status: 401 });
|
|
953
|
+
|
|
954
|
+
const user = decoderTokenToClaims(jwt); // usuario que esta enviando atualiza\xE7\xE3o
|
|
955
|
+
const body = await req.json(); // a atualiza\xE7\xE3o
|
|
956
|
+
|
|
957
|
+
// Atualiza o usu\xE1rio
|
|
958
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_BACKEND_URL}/users/\${user?.id}\`, {
|
|
959
|
+
method: "PATCH",
|
|
960
|
+
headers: {
|
|
961
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
962
|
+
"Content-Type": "application/json",
|
|
963
|
+
},
|
|
964
|
+
body: JSON.stringify(body),
|
|
965
|
+
});
|
|
966
|
+
|
|
967
|
+
if (!response.ok) {
|
|
968
|
+
return NextResponse.json({ message: "Erro ao atualizar usu\xE1rio." }, { status: response.status });
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Agora pede um novo accessToken atualizado
|
|
972
|
+
const refreshRes = await fetch(\`\${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/refresh\`, {
|
|
973
|
+
method: "POST",
|
|
974
|
+
headers: { "Content-Type": "application/json" },
|
|
975
|
+
body: JSON.stringify({ refreshToken: refreshJwt, clientType: "web" }),
|
|
976
|
+
});
|
|
977
|
+
|
|
978
|
+
if (!refreshRes.ok) {
|
|
979
|
+
return NextResponse.json({ message: "Erro ao atualizar token." }, { status: refreshRes.status });
|
|
980
|
+
}
|
|
981
|
+
const newTokens = await refreshRes.json();
|
|
982
|
+
(await cookieStore).set("jwt_back", newTokens.accessToken, { httpOnly: true, sameSite: "lax", path: "/", maxAge: 3600, });
|
|
983
|
+
const decoded = decoderTokenToClaims(newTokens.accessToken);
|
|
984
|
+
|
|
985
|
+
// Retorna tudo pro front (pra atualizar a sess\xE3o)
|
|
986
|
+
return NextResponse.json({ user: { id: decoded?.id, name: decoded?.username, email: decoded?.email, image: decoded?.imagem, userStatus: decoded?.userStatus, }, accessToken: newTokens.accessToken }, { status: 200 });
|
|
987
|
+
} catch (error) {
|
|
988
|
+
console.error(error);
|
|
989
|
+
return NextResponse.json({ message: "Erro ao conectar no backend." }, { status: 500 });
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
`;
|
|
993
|
+
|
|
994
|
+
// src/templates/routes/create-resource-route.ts
|
|
995
|
+
var createResourceTemplate = (resourceInPlural) => `
|
|
996
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
997
|
+
import { cookies } from "next/headers";
|
|
998
|
+
import { decoderTokenToClaims } from "../auth/decode-claims";
|
|
999
|
+
|
|
1000
|
+
export async function POST(req: NextRequest): Promise<NextResponse> {
|
|
1001
|
+
|
|
1002
|
+
try {
|
|
1003
|
+
const cookieStore = cookies();
|
|
1004
|
+
const token = (await cookieStore).get("jwt_back");
|
|
1005
|
+
let jwt = !token ? "not found" : token.value;
|
|
1006
|
+
|
|
1007
|
+
const refreshtoken = (await cookieStore).get("jwt_back_refresh");
|
|
1008
|
+
let refreshJwt = !refreshtoken ? "not found" : refreshtoken.value;
|
|
1009
|
+
if (!jwt || !refreshJwt) return NextResponse.json({ message: "Token n\xE3o encontrado." }, { status: 401 });
|
|
1010
|
+
|
|
1011
|
+
const user = decoderTokenToClaims(jwt);
|
|
1012
|
+
const body = await req.json();
|
|
1013
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/${resourceInPlural.toLocaleLowerCase()}\`, {
|
|
1014
|
+
method: 'POST',
|
|
1015
|
+
headers: {
|
|
1016
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
1017
|
+
'Content-Type': 'application/json',
|
|
1018
|
+
},
|
|
1019
|
+
body: JSON.stringify({ ...body, userId: user?.id }),
|
|
1020
|
+
});
|
|
1021
|
+
const contentType = response.headers.get("Content-Type");
|
|
1022
|
+
if (contentType && contentType.includes("application/json") && response.status === 201) {
|
|
1023
|
+
const post = await response.json();
|
|
1024
|
+
return NextResponse.json(post, { status: 201 });
|
|
1025
|
+
}
|
|
1026
|
+
return NextResponse.json({ message: "The request contains errors, such as invalid data, incorrect format, or missing required fields." }, { status: 400 });
|
|
1027
|
+
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
throw new Error("Erro ao conectar no backend.");
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
`;
|
|
1033
|
+
|
|
1034
|
+
// src/templates/routes/update-resource-route.ts
|
|
1035
|
+
var updateResourceTemplate = (resourceInSigular, resourceInPlural) => `
|
|
1036
|
+
import { cookies } from "next/headers";
|
|
1037
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
1038
|
+
|
|
1039
|
+
export async function POST(req: NextRequest, context: { params: Promise<{ ${resourceInSigular.toLocaleLowerCase()}Id: string }> }) {
|
|
1040
|
+
const { ${resourceInSigular.toLocaleLowerCase()}Id } = await context.params;
|
|
1041
|
+
|
|
1042
|
+
try {
|
|
1043
|
+
const cookieStore = cookies();
|
|
1044
|
+
const token = (await cookieStore).get("jwt_back");
|
|
1045
|
+
let jwt = !token ? "not found" : token.value;
|
|
1046
|
+
|
|
1047
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_BACKEND_URL}/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSigular.toLocaleLowerCase()}Id}\`, {
|
|
1048
|
+
method: "PATCH",
|
|
1049
|
+
headers: {
|
|
1050
|
+
Authorization: \`Bearer \${jwt}\`,
|
|
1051
|
+
"Content-Type": "application/json",
|
|
1052
|
+
},
|
|
1053
|
+
body: JSON.stringify(await req.json()),
|
|
1054
|
+
});
|
|
1055
|
+
|
|
1056
|
+
if (response.status === 200) {
|
|
1057
|
+
const post = await response.json();
|
|
1058
|
+
return NextResponse.json(post, { status: 200 });
|
|
1059
|
+
}
|
|
1060
|
+
return NextResponse.json({ message: "${resourceInPlural.toLocaleLowerCase()} not exists" }, { status: 404 });
|
|
1061
|
+
} catch (error) {
|
|
1062
|
+
throw new Error("Erro ao conectar no backend.");
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
`;
|
|
1066
|
+
|
|
1067
|
+
// src/templates/forms/forgot-form.ts
|
|
1068
|
+
var formForgotTemplate = () => `
|
|
1069
|
+
"use client";
|
|
1070
|
+
|
|
1071
|
+
import Link from "next/link";
|
|
1072
|
+
import { useRouter } from "next/navigation";
|
|
1073
|
+
import { useState } from "react";
|
|
1074
|
+
import { toast } from "react-toastify";
|
|
1075
|
+
|
|
1076
|
+
type Response = { message: string, statusCode: number };
|
|
1077
|
+
export async function sendEmailServerSideProps(email: string): Promise<Response> {
|
|
1078
|
+
try {
|
|
1079
|
+
const response = await fetch(\`\${ process.env.NEXT_PUBLIC_BACKEND_URL }/auth/sendEmail\`, {
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
headers: { "Content-Type": "application/json" },
|
|
1082
|
+
body: JSON.stringify({ email }),
|
|
1083
|
+
cache: "no-store",
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
if (response.ok) {
|
|
1087
|
+
return { message: "Email sent successfully.", statusCode: 200 };
|
|
1088
|
+
} else if (response.status === 404) {
|
|
1089
|
+
return { message: "This email address is not registered in the system.", statusCode: 404 };
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
return { message: "Incorrect email format.", statusCode: 400 };
|
|
1093
|
+
} catch {
|
|
1094
|
+
return { message: "The server is not responding.", statusCode: 500 };
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
export const FormForgot = () => {
|
|
1099
|
+
const router = useRouter();
|
|
1100
|
+
const [data, setData] = useState<Response>({ statusCode: 0, message: "" });
|
|
1101
|
+
|
|
1102
|
+
const handleSubmit = async (e: React.SyntheticEvent<HTMLFormElement>) => {
|
|
1103
|
+
e.preventDefault();
|
|
1104
|
+
|
|
1105
|
+
const form = e.currentTarget;
|
|
1106
|
+
const emailInput = form.elements.namedItem("email") as HTMLInputElement | null;
|
|
1107
|
+
|
|
1108
|
+
if (!emailInput) {
|
|
1109
|
+
toast.error("Email field not found.");
|
|
1110
|
+
return;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
const email = emailInput.value;
|
|
1114
|
+
|
|
1115
|
+
const data = await sendEmailServerSideProps(email);
|
|
1116
|
+
setData(data);
|
|
1117
|
+
|
|
1118
|
+
if (data.statusCode !== 200) {
|
|
1119
|
+
toast.error(data.message);
|
|
1120
|
+
} else {
|
|
1121
|
+
router.push("/forgotredef");
|
|
1122
|
+
}
|
|
1123
|
+
};
|
|
1124
|
+
|
|
1125
|
+
return (
|
|
1126
|
+
<div className="w-full max-w-md p-8 space-y-6 bg-white rounded shadow-md">
|
|
1127
|
+
<h2 className="text-2xl font-bold text-center">Forgot Password</h2>
|
|
1128
|
+
|
|
1129
|
+
<form className="space-y-4" onSubmit={handleSubmit}>
|
|
1130
|
+
<label htmlFor="email" className="block text-sm font-medium text-gray-700">
|
|
1131
|
+
Email
|
|
1132
|
+
</label>
|
|
1133
|
+
|
|
1134
|
+
<input id="email" name="email" type="email" required className="w-full px-3 py-2 mt-1 border rounded shadow-sm focus:outline-none focus:ring focus:border-blue-300"/>
|
|
1135
|
+
|
|
1136
|
+
<button type="submit" className="w-full px-4 py-2 font-bold text-white bg-blue-500 rounded hover:bg-blue-700">
|
|
1137
|
+
Send
|
|
1138
|
+
</button>
|
|
1139
|
+
|
|
1140
|
+
<div className="mt-3 flex justify-between">
|
|
1141
|
+
<Link href="/" className="text-sm text-blue-500 hover:underline">
|
|
1142
|
+
Back to login
|
|
1143
|
+
</Link>
|
|
1144
|
+
<Link href="/cadastro" className="text-sm text-blue-500 hover:underline">
|
|
1145
|
+
Create account
|
|
1146
|
+
</Link>
|
|
1147
|
+
</div>
|
|
1148
|
+
</form>
|
|
1149
|
+
</div>
|
|
1150
|
+
);
|
|
1151
|
+
};
|
|
1152
|
+
`;
|
|
1153
|
+
|
|
1154
|
+
// src/templates/forms/login-form.ts
|
|
1155
|
+
var formLoginTemplate = () => `
|
|
1156
|
+
"use client";
|
|
1157
|
+
|
|
1158
|
+
import { startTransition } from "react";
|
|
1159
|
+
import { signIn, signOut } from "next-auth/react";
|
|
1160
|
+
import { useId, useActionState, useEffect, useState } from "react";
|
|
1161
|
+
import { useForm } from "react-hook-form";
|
|
1162
|
+
import { toast } from "react-toastify";
|
|
1163
|
+
import { usePathname } from "next/navigation";
|
|
1164
|
+
import Link from "next/link";
|
|
1165
|
+
|
|
1166
|
+
import { hiddenPaths } from "./hidenpath";
|
|
1167
|
+
import { ButtonGeneric } from "@/components/ButtonGeneric";
|
|
1168
|
+
|
|
1169
|
+
type TypeLoginData = { email: string; password: string; };
|
|
1170
|
+
|
|
1171
|
+
export const FormLogin: React.FC = () => {
|
|
1172
|
+
const pathname = usePathname();
|
|
1173
|
+
const [mounted, setMounted] = useState(false);
|
|
1174
|
+
useEffect(() => setMounted(true), []);
|
|
1175
|
+
|
|
1176
|
+
const idEmail = useId();
|
|
1177
|
+
const idPassword = useId();
|
|
1178
|
+
const { register, handleSubmit, formState: { errors } } = useForm<TypeLoginData>();
|
|
1179
|
+
|
|
1180
|
+
const [state, setState, isPending] = useActionState(
|
|
1181
|
+
async (prevState: any, data: TypeLoginData) => {
|
|
1182
|
+
try {
|
|
1183
|
+
const response = await signIn("credentials", { redirect: false, ...data, callbackUrl: "/manager" });
|
|
1184
|
+
|
|
1185
|
+
if (response?.status === 401) {
|
|
1186
|
+
toast.error("invalid credentials");
|
|
1187
|
+
return { success: false };
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
if (response?.url) {
|
|
1191
|
+
window.location.href = response.url;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
return { success: true };
|
|
1195
|
+
} catch (error) {
|
|
1196
|
+
toast.error(
|
|
1197
|
+
error instanceof Error ? error.message : "error authenticating"
|
|
1198
|
+
);
|
|
1199
|
+
return { success: false };
|
|
1200
|
+
}
|
|
1201
|
+
},
|
|
1202
|
+
{ success: false }
|
|
1203
|
+
);
|
|
1204
|
+
|
|
1205
|
+
const onSubmitForm = (data: TypeLoginData) => {
|
|
1206
|
+
startTransition(() => { setState(data); });
|
|
1207
|
+
};
|
|
1208
|
+
|
|
1209
|
+
const handleLogout = async () => {
|
|
1210
|
+
await signOut({ callbackUrl: "/" });
|
|
1211
|
+
};
|
|
1212
|
+
|
|
1213
|
+
if (!mounted) {
|
|
1214
|
+
return <div style={{ height: 35 }} />;
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
const isHidden = hiddenPaths.some((path) => pathname.startsWith(path));
|
|
1218
|
+
if (isHidden) { return <ButtonGeneric label="Sair" onClick={handleLogout} />; };
|
|
1219
|
+
|
|
1220
|
+
return (
|
|
1221
|
+
<form onSubmit={handleSubmit(onSubmitForm)} className="w-full flex flex-col items-center gap-3 lg:gap-3">
|
|
1222
|
+
{/* Email */}
|
|
1223
|
+
<div className="flex flex-col lg:flex-row lg:items-center max-h-8.75 gap-2">
|
|
1224
|
+
<label htmlFor={idEmail} className="text-sm font-semibold text-(--text-color)">e-mail:</label>
|
|
1225
|
+
<input id={idEmail} type="text" {...register("email", { required: "Este campo \xE9 obrigat\xF3rio" })}
|
|
1226
|
+
className="min-h-7.5 max-w-30 lg:max-w-30 text-(--text-color) px-3 border border-gray-300 rounded-full outline-none text-sm bg-(--bg-color) caret-(--text-color) text-center transition-all duration-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 w-full lg:w-auto"
|
|
1227
|
+
/>
|
|
1228
|
+
{errors.email && (
|
|
1229
|
+
<span className="text-red-600 text-sm mt-2">
|
|
1230
|
+
{errors.email.message}
|
|
1231
|
+
</span>
|
|
1232
|
+
)}
|
|
1233
|
+
</div>
|
|
1234
|
+
|
|
1235
|
+
{/* Password */}
|
|
1236
|
+
<div className="flex flex-col lg:flex-row lg:items-center max-h-8.75 gap-2">
|
|
1237
|
+
<label htmlFor={idPassword} className="text-sm font-semibold text-(--text-color)">password:</label>
|
|
1238
|
+
<input id={idPassword} type="password" {...register("password", { required: "Este campo \xE9 obrigat\xF3rio" })}
|
|
1239
|
+
className="min-h-7.5 max-w-30 lg:max-w-30 text-(--text-color) px-3 border border-gray-300 rounded-full outline-none text-sm bg-(--bg-color) caret-(--text-color) text-center transition-all duration-200 focus:border-blue-500 focus:ring-2 focus:ring-blue-200 w-full lg:w-auto"
|
|
1240
|
+
/>
|
|
1241
|
+
{errors.password && (
|
|
1242
|
+
<span className="text-red-600 text-sm mt-2">
|
|
1243
|
+
{errors.password.message}
|
|
1244
|
+
</span>
|
|
1245
|
+
)}
|
|
1246
|
+
</div>
|
|
1247
|
+
|
|
1248
|
+
{/* Submit */}
|
|
1249
|
+
<div className="mt-2 lg:mt-0">
|
|
1250
|
+
<ButtonGeneric type="submit" label={isPending ? "Entrando..." : "Entrar"} disabled={isPending} className={\`w-full lg:w-auto justify-center \${isPending ? "opacity-70 cursor-not-allowed" : ""}\`} />
|
|
1251
|
+
</div>
|
|
1252
|
+
|
|
1253
|
+
{/* Forgot password */}
|
|
1254
|
+
<div className="h-full flex flex-col justify-center items-center mt-2 lg:mt-0 text-sm">
|
|
1255
|
+
<Link href="/cadastro" className="text-blue-700 cursor-pointer">cadastre-se</Link>
|
|
1256
|
+
<Link href="/forgotpassword" className="text-blue-700 cursor-pointer">esqueceu sua senha?</Link>
|
|
1257
|
+
</div>
|
|
1258
|
+
</form>
|
|
1259
|
+
);
|
|
1260
|
+
};
|
|
1261
|
+
`;
|
|
1262
|
+
|
|
1263
|
+
// src/templates/forms/redef-form.ts
|
|
1264
|
+
var formRedefTemplate = () => `
|
|
1265
|
+
"use client";
|
|
1266
|
+
|
|
1267
|
+
import { useForm, FormProvider } from 'react-hook-form';
|
|
1268
|
+
import { useRouter } from 'next/navigation';
|
|
1269
|
+
import { yupResolver } from '@hookform/resolvers/yup';
|
|
1270
|
+
import { formSchema } from './formredef-scheme';
|
|
1271
|
+
import { InputCustom } from '@/components/Inputs/InputCustom';
|
|
1272
|
+
|
|
1273
|
+
type Response = { message: string, statusCode: number };
|
|
1274
|
+
interface IFormInputRedem { token: string; password: string; confirpassword: string; };
|
|
1275
|
+
export async function sendRedemServerSideProps(data: IFormInputRedem): Promise<Response> {
|
|
1276
|
+
try {
|
|
1277
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_BACKEND_URL}/auth/redefinir\`, {
|
|
1278
|
+
method: "PUT",
|
|
1279
|
+
headers: {
|
|
1280
|
+
"Content-Type": "application/json",
|
|
1281
|
+
},
|
|
1282
|
+
body: JSON.stringify({ token: data.token, password: data.password, rePassword: data.confirpassword }),
|
|
1283
|
+
});
|
|
1284
|
+
|
|
1285
|
+
if (response.ok) return { message: "Password reset successfully.", statusCode: 200 } as Response;
|
|
1286
|
+
return { message: "User not found, or invalid token.", statusCode: 404 } as Response;
|
|
1287
|
+
|
|
1288
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
1289
|
+
} catch (error) {
|
|
1290
|
+
// Erro de rede pode ocorrer nenhum status \xE9 retornado
|
|
1291
|
+
return { message: "The server stopped responding", statusCode: 500 } as Response;
|
|
1292
|
+
}
|
|
1293
|
+
};
|
|
1294
|
+
|
|
1295
|
+
export const FormRedef = () => {
|
|
1296
|
+
const router = useRouter();
|
|
1297
|
+
const methods = useForm<IFormInputRedem>({ resolver: yupResolver(formSchema) , mode: 'onChange', defaultValues: { token: '', password: '', confirpassword: '' } });
|
|
1298
|
+
|
|
1299
|
+
const onSubmit = async (data: IFormInputRedem) => {
|
|
1300
|
+
const response = await sendRedemServerSideProps(data);
|
|
1301
|
+
if (response.statusCode === 200) {
|
|
1302
|
+
router.push("/login");
|
|
1303
|
+
}
|
|
1304
|
+
};
|
|
1305
|
+
|
|
1306
|
+
return (
|
|
1307
|
+
<FormProvider {...methods}>
|
|
1308
|
+
<form onSubmit={methods.handleSubmit(onSubmit)} className="space-y-4 p-6 bg-white shadow-md rounded-md">
|
|
1309
|
+
<InputCustom name="token" label="C\xF3digo" required={true} />
|
|
1310
|
+
<InputCustom name="password" label="Senha" type="password" required={true} />
|
|
1311
|
+
<InputCustom name="confirpassword" label="Confirme a senha" type="password" required={true} />
|
|
1312
|
+
|
|
1313
|
+
<button type="submit" className="w-full p-2 bg-blue-500 text-white rounded-md hover:bg-blue-600">Send</button>
|
|
1314
|
+
</form>
|
|
1315
|
+
</FormProvider>
|
|
1316
|
+
);
|
|
1317
|
+
}
|
|
1318
|
+
`;
|
|
1319
|
+
|
|
1320
|
+
// src/templates/forms/update-user-form.ts
|
|
1321
|
+
var formUpdateUserTemplate = () => `
|
|
1322
|
+
"use client";
|
|
1323
|
+
|
|
1324
|
+
import { useForm, FormProvider, type Resolver } from "react-hook-form";
|
|
1325
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
1326
|
+
import { useRouter } from "next/navigation";
|
|
1327
|
+
import { useState } from "react";
|
|
1328
|
+
import { toast } from "react-toastify";
|
|
1329
|
+
import { useSession } from "next-auth/react";
|
|
1330
|
+
import { updateFormSchema, UpdateFormSchemaType } from "./formupdateuser-scheme";
|
|
1331
|
+
import { InputCustom } from "@/components/Inputs/InputCustom";
|
|
1332
|
+
import { ButtonGeneric } from "@/components/ButtonGeneric";
|
|
1333
|
+
|
|
1334
|
+
export enum TypeUserStatus { ACTIVE = 'ACTIVE', SUSPENDED = 'SUSPENDED', BANNED = 'BANNED' };
|
|
1335
|
+
export interface UpdateUserInput {
|
|
1336
|
+
photo?: string; bio?: string; skills?: string[];
|
|
1337
|
+
hourly_rate?: number; score?: number; checked?: boolean; userStatus?: TypeUserStatus;
|
|
1338
|
+
}
|
|
1339
|
+
export const FormUpdateUser = () => {
|
|
1340
|
+
const [photoFile, setPhotoFile] = useState<File | null>(null);
|
|
1341
|
+
const router = useRouter();
|
|
1342
|
+
const { data: session, update } = useSession();
|
|
1343
|
+
|
|
1344
|
+
const methods = useForm<UpdateFormSchemaType>({
|
|
1345
|
+
resolver: yupResolver(updateFormSchema) as Resolver<UpdateFormSchemaType>, mode: "onChange",
|
|
1346
|
+
defaultValues: { photo: "", bio: "", skills: "", hourly_rate: null },
|
|
1347
|
+
});
|
|
1348
|
+
|
|
1349
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
1350
|
+
const file = e.target.files?.[0] ?? null;
|
|
1351
|
+
setPhotoFile(file);
|
|
1352
|
+
};
|
|
1353
|
+
|
|
1354
|
+
const handleSubmitUpdate = async (data: UpdateFormSchemaType) => {
|
|
1355
|
+
try {
|
|
1356
|
+
const payload: UpdateUserInput = {
|
|
1357
|
+
photo: photoFile?.name ?? undefined,
|
|
1358
|
+
bio: data.bio ?? undefined,
|
|
1359
|
+
skills: data.skills?.split(","),
|
|
1360
|
+
hourly_rate: data.hourly_rate ?? undefined,
|
|
1361
|
+
score: undefined,
|
|
1362
|
+
checked: undefined,
|
|
1363
|
+
userStatus: undefined,
|
|
1364
|
+
};
|
|
1365
|
+
|
|
1366
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_URL}/api/users/update\`, {
|
|
1367
|
+
method: "POST",
|
|
1368
|
+
headers: { "Content-Type": "application/json" },
|
|
1369
|
+
body: JSON.stringify(payload),
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
if (!response.ok) {
|
|
1373
|
+
const errorData = await response.json();
|
|
1374
|
+
toast.error(errorData.message || "Erro ao atualizar informa\xE7\xF5es do usu\xE1rio");
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
const usersession = await response.json();
|
|
1378
|
+
// \u{1F539} Upload da foto (caso exista)
|
|
1379
|
+
if (photoFile && usersession) {
|
|
1380
|
+
const formData = new FormData();
|
|
1381
|
+
formData.append("photo", photoFile, photoFile.name);
|
|
1382
|
+
const uploadResp = await fetch(\`\${process.env.NEXT_PUBLIC_URL}/api/users/photo\`, { method: "POST", body: formData });
|
|
1383
|
+
if (uploadResp.ok) {
|
|
1384
|
+
const userAtualizado = await uploadResp.json();
|
|
1385
|
+
const newSession = await update({ user: { ...session?.user, image: userAtualizado.photo, } });
|
|
1386
|
+
if (newSession) {
|
|
1387
|
+
toast.success("Dados do usu\xE1rio atualizados com sucesso!");
|
|
1388
|
+
}
|
|
1389
|
+
} else {
|
|
1390
|
+
toast.error("Erro ao enviar a foto");
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
router.refresh();
|
|
1394
|
+
router.push("/manager");
|
|
1395
|
+
} catch (error) {
|
|
1396
|
+
console.error(error);
|
|
1397
|
+
toast.error("Erro de comunica\xE7\xE3o com o servidor");
|
|
1398
|
+
}
|
|
1399
|
+
};
|
|
1400
|
+
|
|
1401
|
+
return (
|
|
1402
|
+
<FormProvider {...methods}>
|
|
1403
|
+
<form onSubmit={methods.handleSubmit(handleSubmitUpdate)}
|
|
1404
|
+
className="flex flex-col gap-6 sm:p-6 lg:p-8 w-full bg-white/80 backdrop-blur-md rounded-3xl shadow-[0_8px_30px_rgb(0,0,0,0.12)] border border-gray-100 max-w-[90%] mx-auto"
|
|
1405
|
+
>
|
|
1406
|
+
{/* Foto */}
|
|
1407
|
+
<div>
|
|
1408
|
+
<label className="block text-sm font-medium text-gray-700">Foto do usu\xE1rio</label>
|
|
1409
|
+
<input type="file" accept="image/*" onChange={handleFileChange} className="mt-2 border border-gray-300 rounded-lg p-2 w-full" />
|
|
1410
|
+
{photoFile && <p className="text-sm text-gray-600 mt-1">{photoFile.name}</p>}
|
|
1411
|
+
</div>
|
|
1412
|
+
|
|
1413
|
+
{/* Bio */}
|
|
1414
|
+
<InputCustom name="bio" label="Biografia" type="text" />
|
|
1415
|
+
|
|
1416
|
+
{/* Skills */}
|
|
1417
|
+
<InputCustom name="skills" label="Habilidades (separadas por v\xEDrgula)" type="text" />
|
|
1418
|
+
|
|
1419
|
+
{/* Hourly Rate */}
|
|
1420
|
+
<InputCustom name="hourly_rate" label="Valor por hora" type="number" />
|
|
1421
|
+
|
|
1422
|
+
{/* Bot\xE3o */}
|
|
1423
|
+
<ButtonGeneric type="submit" label="Atualizar Usu\xE1rio" />
|
|
1424
|
+
</form>
|
|
1425
|
+
</FormProvider>
|
|
1426
|
+
);
|
|
1427
|
+
};
|
|
1428
|
+
`;
|
|
1429
|
+
|
|
1430
|
+
// src/templates/forms/register-form.ts
|
|
1431
|
+
var formRegisterTemplate = () => `
|
|
1432
|
+
"use client";
|
|
1433
|
+
|
|
1434
|
+
import { useForm, FormProvider } from "react-hook-form";
|
|
1435
|
+
import { useRouter } from "next/navigation";
|
|
1436
|
+
import { yupResolver } from "@hookform/resolvers/yup";
|
|
1437
|
+
import { formSchema, FormSchemaType } from "./formregister-scheme";
|
|
1438
|
+
import { toast } from "react-toastify";
|
|
1439
|
+
import { InputCustom } from "@/components/Inputs/InputCustom";
|
|
1440
|
+
import { ButtonGeneric } from "@/components/ButtonGeneric";
|
|
1441
|
+
|
|
1442
|
+
export const FormRegister = () => {
|
|
1443
|
+
const router = useRouter();
|
|
1444
|
+
const methods = useForm<FormSchemaType>({
|
|
1445
|
+
resolver: yupResolver(formSchema), mode: "onChange",
|
|
1446
|
+
defaultValues: { name: "", email: "", cpf: "", dateOfBirth: new Date(), password: "", repeatPassword: "", phone: "", country: "", state: "", city: "", address: "" },
|
|
1447
|
+
});
|
|
1448
|
+
|
|
1449
|
+
const handlesubmitRegister = async (data: FormSchemaType) => {
|
|
1450
|
+
try {
|
|
1451
|
+
const response = await fetch(\`\${process.env.NEXT_PUBLIC_URL}/api/users\`, {
|
|
1452
|
+
method: "POST",
|
|
1453
|
+
headers: { "Content-Type": "application/json" },
|
|
1454
|
+
body: JSON.stringify(data),
|
|
1455
|
+
});
|
|
1456
|
+
|
|
1457
|
+
if (response && response.status === 201) {
|
|
1458
|
+
toast.success("User created successfully.");
|
|
1459
|
+
router.push("/");
|
|
1460
|
+
} else {
|
|
1461
|
+
toast.error(response.status === 400 ? "Email already exists in the application, proceed to password reset" : "Error registering");
|
|
1462
|
+
}
|
|
1463
|
+
} catch (error) {
|
|
1464
|
+
throw new Error("There was a communication.");
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
|
|
1468
|
+
return (
|
|
1469
|
+
<FormProvider {...methods}>
|
|
1470
|
+
<form onSubmit={methods.handleSubmit(handlesubmitRegister)} className="flex flex-col gap-4 p-6 bg-white rounded-md w-full max-w-[80%] mx-auto">
|
|
1471
|
+
{/* Linha 1 */}
|
|
1472
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
1473
|
+
<InputCustom name="name" label="Nome completo" required />
|
|
1474
|
+
<InputCustom name="email" label="Email" type="email" required />
|
|
1475
|
+
</div>
|
|
1476
|
+
|
|
1477
|
+
{/* Linha 2 */}
|
|
1478
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
1479
|
+
<InputCustom name="cpf" label="CPF" required />
|
|
1480
|
+
<InputCustom name="dateOfBirth" label="Data de nascimento" type="date" asDate required />
|
|
1481
|
+
<InputCustom name="phone" label="Telefone" required />
|
|
1482
|
+
</div>
|
|
1483
|
+
|
|
1484
|
+
{/* Linha 3 */}
|
|
1485
|
+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
1486
|
+
<InputCustom name="country" label="Pa\xEDs" required />
|
|
1487
|
+
<InputCustom name="state" label="Estado" required />
|
|
1488
|
+
<InputCustom name="city" label="Cidade" required />
|
|
1489
|
+
</div>
|
|
1490
|
+
|
|
1491
|
+
{/* Linha 4 */}
|
|
1492
|
+
<InputCustom name="address" label="Endere\xE7o" required />
|
|
1493
|
+
|
|
1494
|
+
{/* Linha 5 - Senhas */}
|
|
1495
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
1496
|
+
<InputCustom name="password" label="Senha" type="password" required />
|
|
1497
|
+
<InputCustom name="repeatPassword" label="Confirme sua senha" type="password" required />
|
|
1498
|
+
</div>
|
|
1499
|
+
|
|
1500
|
+
{/* Bot\xE3o */}
|
|
1501
|
+
<div className="flex justify-center mt-4">
|
|
1502
|
+
<ButtonGeneric label="Registrar" />
|
|
1503
|
+
</div>
|
|
1504
|
+
</form>
|
|
1505
|
+
</FormProvider>
|
|
1506
|
+
);
|
|
1507
|
+
};
|
|
1508
|
+
|
|
1509
|
+
`;
|
|
1510
|
+
|
|
1511
|
+
// src/templates/forms/login-wrapper-form.ts
|
|
1512
|
+
var formLoginWrapperTemplate = () => `
|
|
1513
|
+
"use client";
|
|
1514
|
+
|
|
1515
|
+
import { useState } from "react";
|
|
1516
|
+
import { usePathname } from "next/navigation";
|
|
1517
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
1518
|
+
import { FormLogin } from ".";
|
|
1519
|
+
import { XIcon } from "lucide-react";
|
|
1520
|
+
import { hiddenPaths } from "./hidenpath";
|
|
1521
|
+
import { ButtonGeneric } from "@/components/ButtonGeneric";
|
|
1522
|
+
|
|
1523
|
+
export const FormLoginWrapper = () => {
|
|
1524
|
+
const pathname = usePathname();
|
|
1525
|
+
const [open, setOpen] = useState(false);
|
|
1526
|
+
|
|
1527
|
+
const isHidden = hiddenPaths.some((path) => pathname.startsWith(path));
|
|
1528
|
+
|
|
1529
|
+
// If the route is protected \u2192 only the FormLogin itself is rendered (which displays the "Log Out" button).
|
|
1530
|
+
if (isHidden) {
|
|
1531
|
+
return <FormLogin />;
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
return (
|
|
1535
|
+
<>
|
|
1536
|
+
{/* Mobile & Tablets (<lg): displays normal form */}
|
|
1537
|
+
<div className="bg-(--bg-color) block md:hidden">
|
|
1538
|
+
<FormLogin />
|
|
1539
|
+
</div>
|
|
1540
|
+
|
|
1541
|
+
{/* Desktop (lg:): button + modal */}
|
|
1542
|
+
<div className="hidden md:flex">
|
|
1543
|
+
<ButtonGeneric label="Login" onClick={() => setOpen(true)} />
|
|
1544
|
+
</div>
|
|
1545
|
+
|
|
1546
|
+
{/* MODAL */}
|
|
1547
|
+
<AnimatePresence>
|
|
1548
|
+
{open && (
|
|
1549
|
+
<motion.div className="fixed inset-0 bg-black/40 backdrop-blur-sm flex items-center justify-center z-999" initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}>
|
|
1550
|
+
<motion.div className="bg-(--bg-color) rounded-2xl p-6 w-105 shadow-xl relative" initial={{ scale: 0.8, opacity: 0 }} animate={{ scale: 1, opacity: 1 }} exit={{ scale: 0.8, opacity: 0 }} transition={{ type: "spring", stiffness: 120 }}>
|
|
1551
|
+
{/* Bot\xE3o fechar */}
|
|
1552
|
+
<button onClick={() => setOpen(false)} className="absolute right-4 top-4 text-gray-600 hover:text-black text-xl">
|
|
1553
|
+
<XIcon />
|
|
1554
|
+
</button>
|
|
1555
|
+
|
|
1556
|
+
<h2 className="text-lg font-semibold mb-4">Login</h2>
|
|
1557
|
+
|
|
1558
|
+
{/* Form within the modal */}
|
|
1559
|
+
<FormLogin />
|
|
1560
|
+
</motion.div>
|
|
1561
|
+
</motion.div>
|
|
1562
|
+
)}
|
|
1563
|
+
</AnimatePresence>
|
|
1564
|
+
</>
|
|
1565
|
+
);
|
|
1566
|
+
};
|
|
1567
|
+
|
|
1568
|
+
`;
|
|
1569
|
+
|
|
1570
|
+
// src/utils/fs.ts
|
|
1571
|
+
import fs from "fs";
|
|
1572
|
+
import path from "path";
|
|
1573
|
+
var createDir = (dirPath) => {
|
|
1574
|
+
if (!fs.existsSync(dirPath)) {
|
|
1575
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
1576
|
+
}
|
|
1577
|
+
;
|
|
1578
|
+
};
|
|
1579
|
+
var createFile = (filePath, content) => {
|
|
1580
|
+
if (!fs.existsSync(filePath)) {
|
|
1581
|
+
fs.writeFileSync(filePath, content.trimStart());
|
|
1582
|
+
}
|
|
1583
|
+
};
|
|
1584
|
+
function pathExists(p) {
|
|
1585
|
+
return fs.existsSync(p);
|
|
1586
|
+
}
|
|
1587
|
+
var moveFile = (fromPath, toPath) => {
|
|
1588
|
+
if (fs.existsSync(fromPath)) {
|
|
1589
|
+
const toDir = path.dirname(toPath);
|
|
1590
|
+
if (!fs.existsSync(toDir)) {
|
|
1591
|
+
fs.mkdirSync(toDir, { recursive: true });
|
|
1592
|
+
}
|
|
1593
|
+
fs.renameSync(fromPath, toPath);
|
|
1594
|
+
}
|
|
1595
|
+
};
|
|
1596
|
+
|
|
1597
|
+
// src/utils/string.ts
|
|
1598
|
+
var capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1);
|
|
1599
|
+
|
|
1600
|
+
// src/templates/pages/update-page.ts
|
|
1601
|
+
var updatePageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
1602
|
+
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
|
|
1603
|
+
import { FormEdit${resourceInSingular} } from "@/forms/${resourceInPlural.toLocaleLowerCase()}";
|
|
1604
|
+
//import { ${resourceInSingular} } from "@/utils/models/${resourceInPlural.toLocaleLowerCase()}";
|
|
1605
|
+
import { Session } from "@/utils/route";
|
|
1606
|
+
import { getServerSession } from "next-auth";
|
|
1607
|
+
import { redirect } from "next/navigation";
|
|
1608
|
+
|
|
1609
|
+
type ${resourceInSingular} = {}
|
|
1610
|
+
interface PageProps { params: { ${resourceInSingular.toLocaleLowerCase()}Id: string; }; };
|
|
1611
|
+
|
|
1612
|
+
async function get${resourceInSingular}ById(${resourceInSingular.toLocaleLowerCase()}Id: string, token: string): Promise< ${resourceInSingular} | null> {
|
|
1613
|
+
try {
|
|
1614
|
+
const response = await fetch(\`\${process.env.NEXT_BACKEND_URL}/${resourceInPlural.toLocaleLowerCase()}/\${${resourceInSingular.toLocaleLowerCase()}Id}\`,
|
|
1615
|
+
{
|
|
1616
|
+
method: "GET",
|
|
1617
|
+
cache: "no-store",
|
|
1618
|
+
headers: {
|
|
1619
|
+
Authorization: \`Bearer \${token}\`,
|
|
1620
|
+
},
|
|
1621
|
+
}
|
|
1622
|
+
);
|
|
1623
|
+
if (!response.ok) { return null; };
|
|
1624
|
+
const ${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular} = await response.json();
|
|
1625
|
+
return ${resourceInSingular.toLocaleLowerCase()};
|
|
1626
|
+
} catch (error) {
|
|
1627
|
+
console.error("Erro ao buscar ${resourceInSingular.toLocaleLowerCase()}:", error);
|
|
1628
|
+
return null;
|
|
1629
|
+
}
|
|
1630
|
+
};
|
|
1631
|
+
|
|
1632
|
+
export default async function ${resourceInSingular}EditPage({ params }: PageProps) {
|
|
1633
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = await params;
|
|
1634
|
+
const session: Session | null = await getServerSession(authOptions);
|
|
1635
|
+
if (!session) redirect("/");
|
|
1636
|
+
const ${resourceInSingular.toLocaleLowerCase()}: ${resourceInSingular} | null = await get${resourceInSingular}ById(${resourceInSingular.toLocaleLowerCase()}Id, session.accessToken);
|
|
1637
|
+
return (
|
|
1638
|
+
<div className="w-full min-h-screen bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
1639
|
+
<div className="max-w-3xl mx-auto flex flex-col gap-8">
|
|
1640
|
+
|
|
1641
|
+
{${resourceInSingular.toLocaleLowerCase()} ? (
|
|
1642
|
+
<>
|
|
1643
|
+
{/* Informa\xE7\xF5es do post */}
|
|
1644
|
+
<section className="bg-white/70 dark:bg-black/20 rounded-xl p-6 shadow">
|
|
1645
|
+
<h1 className="text-2xl font-semibold">Editar ${resourceInSingular}</h1>
|
|
1646
|
+
<p className="text-sm opacity-70 mt-1">
|
|
1647
|
+
Este ${resourceInSingular} foi \u2022 Criado em {${resourceInSingular.toLocaleLowerCase()}.createdAt.toString()}
|
|
1648
|
+
</p>
|
|
1649
|
+
</section>
|
|
1650
|
+
|
|
1651
|
+
{/* Formul\xE1rio */}
|
|
1652
|
+
<FormEdit${resourceInSingular} ${resourceInSingular.toLowerCase()}={${resourceInSingular.toLocaleLowerCase()}} />
|
|
1653
|
+
</>
|
|
1654
|
+
) : (
|
|
1655
|
+
<section className="bg-white/70 dark:bg-black/20 rounded-xl p-6 shadow text-center">
|
|
1656
|
+
<h1 className="text-xl font-semibold">
|
|
1657
|
+
Post n\xE3o encontrado
|
|
1658
|
+
</h1>
|
|
1659
|
+
<p className="text-sm opacity-70 mt-2">
|
|
1660
|
+
O ${resourceInSingular} que voc\xEA est\xE1 tentando editar n\xE3o existe ou foi removido.
|
|
1661
|
+
</p>
|
|
1662
|
+
</section>
|
|
1663
|
+
)}
|
|
1664
|
+
|
|
1665
|
+
</div>
|
|
1666
|
+
</div>
|
|
1667
|
+
);
|
|
1668
|
+
};
|
|
1669
|
+
|
|
1670
|
+
`;
|
|
1671
|
+
|
|
1672
|
+
// src/templates/pages/delete-page.ts
|
|
1673
|
+
var deletePageTemplate = (resourceInSingular, resourceInPlural) => `
|
|
1674
|
+
import { FormDeleteResource } from "@/forms/shared";
|
|
1675
|
+
|
|
1676
|
+
interface PageProps { params: { ${resourceInSingular.toLocaleLowerCase()}Id: string; }; };
|
|
1677
|
+
export default async function ${resourceInSingular}DeletePage({ params }: PageProps) {
|
|
1678
|
+
const { ${resourceInSingular.toLocaleLowerCase()}Id } = await params;
|
|
1679
|
+
|
|
1680
|
+
return <div className="w-full min-h-screen flex flex-col bg-[--bg-section-100] p-10 transition-colors duration-500">
|
|
1681
|
+
<h2 className="text-center">Tem certeza que vc quer deletar esse ${resourceInSingular}?</h2>
|
|
1682
|
+
<FormDeleteResource resource={"${resourceInPlural.toLocaleLowerCase()}"} resourceId={${resourceInSingular.toLocaleLowerCase()}Id} />
|
|
1683
|
+
</div>;
|
|
1684
|
+
}
|
|
1685
|
+
`;
|
|
1686
|
+
|
|
1687
|
+
// src/utils/services/install-dependences.service.ts
|
|
1688
|
+
import { execSync } from "child_process";
|
|
1689
|
+
|
|
1690
|
+
// src/utils/guards/next-verify.guard.ts
|
|
1691
|
+
import fs2 from "fs";
|
|
1692
|
+
import path2 from "path";
|
|
1693
|
+
function nextProjectGuardSimple() {
|
|
1694
|
+
const cwd = process.cwd();
|
|
1695
|
+
const pkgPath = path2.join(cwd, "package.json");
|
|
1696
|
+
if (!fs2.existsSync(pkgPath)) {
|
|
1697
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNenhum package.json encontrado neste diret\xF3rio.");
|
|
1698
|
+
process.exit(1);
|
|
1699
|
+
}
|
|
1700
|
+
const pkg = JSON.parse(fs2.readFileSync(pkgPath, "utf-8"));
|
|
1701
|
+
const hasNext = pkg.dependencies?.next || pkg.devDependencies?.next;
|
|
1702
|
+
if (!hasNext) {
|
|
1703
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mExecute dentro de um projeto Next.js");
|
|
1704
|
+
process.exit(1);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
// src/utils/guards/dependency.guard.ts
|
|
1709
|
+
import fs3 from "fs";
|
|
1710
|
+
import path3 from "path";
|
|
1711
|
+
function hasDependency(pkgName) {
|
|
1712
|
+
const cwd = process.cwd();
|
|
1713
|
+
const pkgPath = path3.join(cwd, "package.json");
|
|
1714
|
+
if (!fs3.existsSync(pkgPath)) {
|
|
1715
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNenhum package.json encontrado.");
|
|
1716
|
+
process.exit(1);
|
|
1717
|
+
}
|
|
1718
|
+
const pkg = JSON.parse(fs3.readFileSync(pkgPath, "utf-8"));
|
|
1719
|
+
return Boolean(
|
|
1720
|
+
pkg.dependencies?.[pkgName] || pkg.devDependencies?.[pkgName] || pkg.peerDependencies?.[pkgName]
|
|
1721
|
+
);
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/utils/services/install-dependences.service.ts
|
|
1725
|
+
var DependencyInstaller = class _DependencyInstaller {
|
|
1726
|
+
constructor() {
|
|
1727
|
+
this.hasInstalled = false;
|
|
1728
|
+
}
|
|
1729
|
+
static getInstance() {
|
|
1730
|
+
if (!_DependencyInstaller.instance) {
|
|
1731
|
+
_DependencyInstaller.instance = new _DependencyInstaller();
|
|
1732
|
+
}
|
|
1733
|
+
return _DependencyInstaller.instance;
|
|
1734
|
+
}
|
|
1735
|
+
async install() {
|
|
1736
|
+
if (this.hasInstalled || hasDependency("lucide-react")) return;
|
|
1737
|
+
this.hasInstalled = true;
|
|
1738
|
+
try {
|
|
1739
|
+
execSync(`
|
|
1740
|
+
npm install lucide-react &&
|
|
1741
|
+
npm install clsx
|
|
1742
|
+
`, { stdio: "inherit" });
|
|
1743
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
1744
|
+
} catch (error) {
|
|
1745
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
1746
|
+
}
|
|
1747
|
+
;
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// src/builders/core/next-accelerate-builder.ts
|
|
1752
|
+
import pluralize from "pluralize";
|
|
1753
|
+
|
|
1754
|
+
// src/utils/services/git.service.ts
|
|
1755
|
+
import { execSync as execSync2 } from "child_process";
|
|
1756
|
+
function gitCommit(message) {
|
|
1757
|
+
if (!message) return;
|
|
1758
|
+
const safeMessage = String.raw`${message.replace(/(["`$\\])/g, "\\$1")}`;
|
|
1759
|
+
try {
|
|
1760
|
+
execSync2("git add .", { stdio: "ignore" });
|
|
1761
|
+
execSync2(`git commit -m "${safeMessage}"`, { stdio: "ignore" });
|
|
1762
|
+
} catch (error) {
|
|
1763
|
+
console.error("Failed to commit changes:", error);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// src/builders/core/next-accelerate-builder.ts
|
|
1768
|
+
var NextAccelerateBuilder = class {
|
|
1769
|
+
constructor(inputName, options) {
|
|
1770
|
+
this.inputName = inputName;
|
|
1771
|
+
this.options = options;
|
|
1772
|
+
this.basePath = process.cwd();
|
|
1773
|
+
this.resource = pluralize(inputName.toLowerCase());
|
|
1774
|
+
this.singular = pluralize.singular(this.resource);
|
|
1775
|
+
}
|
|
1776
|
+
createCommit(message) {
|
|
1777
|
+
if (!this.options?.git) return;
|
|
1778
|
+
gitCommit(message);
|
|
1779
|
+
}
|
|
1780
|
+
build() {
|
|
1781
|
+
if (!this.options?.git) return;
|
|
1782
|
+
console.log("commits made successfully \u2728");
|
|
1783
|
+
}
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
// src/builders/resource-builder.ts
|
|
1787
|
+
var NextResourceBuilder = class extends NextAccelerateBuilder {
|
|
1788
|
+
constructor(inputName, options) {
|
|
1789
|
+
super(inputName, options);
|
|
1790
|
+
}
|
|
1791
|
+
// required for components only
|
|
1792
|
+
installDependencesRequired() {
|
|
1793
|
+
DependencyInstaller.getInstance().install();
|
|
1794
|
+
return this;
|
|
1795
|
+
}
|
|
1796
|
+
setBasePath() {
|
|
1797
|
+
this.basePath = path4.join(process.cwd(), "src/app/(privates)", this.resource);
|
|
1798
|
+
createDir(this.basePath);
|
|
1799
|
+
return this;
|
|
1800
|
+
}
|
|
1801
|
+
setBasePathForComponents() {
|
|
1802
|
+
this.basePath = path4.join(process.cwd(), "src/components");
|
|
1803
|
+
createDir(this.basePath);
|
|
1804
|
+
return this;
|
|
1805
|
+
}
|
|
1806
|
+
createComponentInputCustom() {
|
|
1807
|
+
const componentPath = path4.join(this.basePath, "Inputs/InputCustom");
|
|
1808
|
+
createDir(componentPath);
|
|
1809
|
+
createFile(path4.join(componentPath, "index.tsx"), inputTemplate());
|
|
1810
|
+
if (this.options?.test) {
|
|
1811
|
+
createFile(path4.join(componentPath, "InputCustom.spec.tsx"), inputTestUnitTemplate());
|
|
1812
|
+
createFile(path4.join(componentPath, "renderWithForm.tsx"), inputFormHelperTestUnitTemplate());
|
|
1813
|
+
}
|
|
1814
|
+
;
|
|
1815
|
+
if (this.options?.git) this.createCommit(`feat(input): add input custom component`);
|
|
1816
|
+
return this;
|
|
1817
|
+
}
|
|
1818
|
+
createListPage() {
|
|
1819
|
+
createFile(path4.join(this.basePath, "page.tsx"), listPageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
1820
|
+
return this;
|
|
1821
|
+
}
|
|
1822
|
+
createDetailPage() {
|
|
1823
|
+
const detailDir = path4.join(this.basePath, `[${this.singular}Id]`);
|
|
1824
|
+
createDir(detailDir);
|
|
1825
|
+
createFile(path4.join(detailDir, "page.tsx"), detailPageTemplate(capitalize(this.singular)));
|
|
1826
|
+
const editDir = path4.join(detailDir, "edit");
|
|
1827
|
+
createDir(editDir);
|
|
1828
|
+
createFile(path4.join(editDir, "page.tsx"), updatePageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
1829
|
+
const deleteDir = path4.join(detailDir, "delete");
|
|
1830
|
+
createDir(deleteDir);
|
|
1831
|
+
createFile(path4.join(deleteDir, "page.tsx"), deletePageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
1832
|
+
this.createCommit(`feat(${this.resource}): add all pages for detail view`);
|
|
1833
|
+
return this;
|
|
1834
|
+
}
|
|
1835
|
+
createNewPage() {
|
|
1836
|
+
const dir = path4.join(this.basePath, "new");
|
|
1837
|
+
createDir(dir);
|
|
1838
|
+
createFile(path4.join(dir, "page.tsx"), newPageTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
1839
|
+
return this;
|
|
1840
|
+
}
|
|
1841
|
+
};
|
|
1842
|
+
|
|
1843
|
+
// src/commands/create-resource.ts
|
|
1844
|
+
function createResource(inputName, options) {
|
|
1845
|
+
nextProjectGuardSimple();
|
|
1846
|
+
if (!inputName) {
|
|
1847
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mPlease provide the name of the resource.");
|
|
1848
|
+
process.exit(1);
|
|
1849
|
+
}
|
|
1850
|
+
const builder = new NextResourceBuilder(inputName, options);
|
|
1851
|
+
builder.setBasePath().createListPage().createDetailPage().createNewPage().build();
|
|
1852
|
+
console.log(`Resource "${inputName}" created \x1B[32m\u2714 Success\x1B[0m`);
|
|
1853
|
+
}
|
|
1854
|
+
function createComponents(options) {
|
|
1855
|
+
nextProjectGuardSimple();
|
|
1856
|
+
const builder = new NextResourceBuilder("default", options);
|
|
1857
|
+
builder.installDependencesRequired().setBasePathForComponents().createComponentInputCustom().build();
|
|
1858
|
+
console.log(`Resource "components" created \x1B[32m\u2714 Success\x1B[0m`);
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
// src/builders/resource-form-builder.ts
|
|
1862
|
+
import path5 from "path";
|
|
1863
|
+
|
|
1864
|
+
// src/utils/services/install-dependences-form.service.ts
|
|
1865
|
+
import { execSync as execSync3 } from "child_process";
|
|
1866
|
+
var DependencyFormInstaller = class _DependencyFormInstaller {
|
|
1867
|
+
constructor() {
|
|
1868
|
+
this.hasInstalled = false;
|
|
1869
|
+
}
|
|
1870
|
+
static getInstance() {
|
|
1871
|
+
if (!_DependencyFormInstaller.instance) {
|
|
1872
|
+
_DependencyFormInstaller.instance = new _DependencyFormInstaller();
|
|
1873
|
+
}
|
|
1874
|
+
return _DependencyFormInstaller.instance;
|
|
1875
|
+
}
|
|
1876
|
+
async install() {
|
|
1877
|
+
if (this.hasInstalled || hasDependency("react-hook-form")) return;
|
|
1878
|
+
this.hasInstalled = true;
|
|
1879
|
+
try {
|
|
1880
|
+
execSync3(`
|
|
1881
|
+
npm install react-hook-form &&
|
|
1882
|
+
npm install yup @hookform/resolvers &&
|
|
1883
|
+
npm install -D @types/yup &&
|
|
1884
|
+
npm install react-toastify
|
|
1885
|
+
`, { stdio: "inherit" });
|
|
1886
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
1887
|
+
} catch (error) {
|
|
1888
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
1889
|
+
}
|
|
1890
|
+
;
|
|
1891
|
+
}
|
|
1892
|
+
};
|
|
1893
|
+
|
|
1894
|
+
// src/templates/components/footer-comp.ts
|
|
1895
|
+
var footerTemplate = () => `
|
|
1896
|
+
export const Footer: React.FC = () => {
|
|
1897
|
+
return (
|
|
1898
|
+
<footer className="bg-(--brand-footer) font-sans">
|
|
1899
|
+
<div className="container px-6 py-12 mx-auto">
|
|
1900
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 sm:gap-y-10 lg:grid-cols-4">
|
|
1901
|
+
<div className="sm:col-span-2">
|
|
1902
|
+
<h1 className="max-w-lg text-xl font-semibold tracking-tight text-gray-800 xl:text-2xl dark:text-white">Subscribe our newsletter to get an update.</h1>
|
|
1903
|
+
|
|
1904
|
+
<div className="flex flex-col mx-auto mt-6 space-y-3 md:space-y-0 md:flex-row">
|
|
1905
|
+
<input id="email" type="text" className="px-4 py-2 text-gray-700 border rounded-md bg-(--brand-footer) dark:text-gray-300 dark:border-gray-600 focus:border-blue-400 dark:focus:border-blue-300 focus:outline-none focus:ring focus:ring-opacity-40 focus:ring-blue-300" placeholder="Email Address" />
|
|
1906
|
+
|
|
1907
|
+
<button className="w-full px-6 py-2.5 text-sm font-medium tracking-wider text-white transition-colors duration-300 transform md:w-auto md:mx-4 focus:outline-none bg-gray-800 rounded-lg hover:bg-gray-700 focus:ring focus:ring-gray-300 focus:ring-opacity-80">
|
|
1908
|
+
Subscribe
|
|
1909
|
+
</button>
|
|
1910
|
+
</div>
|
|
1911
|
+
</div>
|
|
1912
|
+
|
|
1913
|
+
<div>
|
|
1914
|
+
<p className="font-semibold text-gray-800 dark:text-white">Quick Link</p>
|
|
1915
|
+
|
|
1916
|
+
<div className="flex flex-col items-start mt-5 space-y-2">
|
|
1917
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Home</p>
|
|
1918
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Who We Are</p>
|
|
1919
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Our Philosophy</p>
|
|
1920
|
+
</div>
|
|
1921
|
+
</div>
|
|
1922
|
+
|
|
1923
|
+
<div>
|
|
1924
|
+
<p className="font-semibold text-gray-800 dark:text-white">Industries</p>
|
|
1925
|
+
|
|
1926
|
+
<div className="flex flex-col items-start mt-5 space-y-2">
|
|
1927
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Retail & E-Commerce</p>
|
|
1928
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Information Technology</p>
|
|
1929
|
+
<p className="text-gray-600 transition-colors duration-300 dark:text-gray-300 dark:hover:text-blue-400 hover:underline hover:cursor-pointer hover:text-blue-500">Finance & Insurance</p>
|
|
1930
|
+
</div>
|
|
1931
|
+
</div>
|
|
1932
|
+
</div>
|
|
1933
|
+
|
|
1934
|
+
<hr className="my-6 border-gray-200 md:my-8 dark:border-gray-700 h-2" />
|
|
1935
|
+
|
|
1936
|
+
<div className="sm:flex sm:items-center sm:justify-between">
|
|
1937
|
+
<div className="flex flex-1 gap-2 hover:cursor-pointer">
|
|
1938
|
+
<img src="https://www.svgrepo.com/show/303139/google-play-badge-logo.svg" width="130" height="110" alt="" />
|
|
1939
|
+
<img src="https://www.svgrepo.com/show/303128/download-on-the-app-store-apple-logo.svg" width="130" height="110" alt="" />
|
|
1940
|
+
</div>
|
|
1941
|
+
|
|
1942
|
+
<div className="flex gap-2 hover:cursor-pointer">
|
|
1943
|
+
<img src="https://www.svgrepo.com/show/303114/facebook-3-logo.svg" width="30" height="30" alt="fb" />
|
|
1944
|
+
<img src="https://www.svgrepo.com/show/303145/instagram-2-1-logo.svg" width="30" height="30" alt="inst" />
|
|
1945
|
+
<img src="https://www.svgrepo.com/show/94698/github.svg" className="" width="30" height="30" alt="gt" />
|
|
1946
|
+
<img src="https://www.svgrepo.com/show/22037/path.svg" width="30" height="30" alt="pn" />
|
|
1947
|
+
<img src="https://www.svgrepo.com/show/28145/linkedin.svg" width="30" height="30" alt="in" />
|
|
1948
|
+
</div>
|
|
1949
|
+
</div>
|
|
1950
|
+
<p className="font-sans p-8 text-start md:text-center md:text-lg md:p-4">
|
|
1951
|
+
\xA9 2025 Template Deloped by <a href="https://github.com/brito-response" target="_blank" rel="noopener noreferrer">Neto.</a></p>
|
|
1952
|
+
</div>
|
|
1953
|
+
</footer>
|
|
1954
|
+
);
|
|
1955
|
+
};
|
|
1956
|
+
|
|
1957
|
+
`;
|
|
1958
|
+
|
|
1959
|
+
// src/templates/components/menu-aside-comp.ts
|
|
1960
|
+
var menuAsideTemplate = () => `
|
|
1961
|
+
"use client";
|
|
1962
|
+
|
|
1963
|
+
import { AlignJustifyIcon, FileEditIcon, FolderTreeIcon, HouseIcon, MessageCircleIcon, SettingsIcon, UserIcon, WalletIcon } from "lucide-react";
|
|
1964
|
+
import Link from "next/link";
|
|
1965
|
+
import { ReactElement, useState } from "react";
|
|
1966
|
+
import { useMenu } from "@/contexts/manager-context";
|
|
1967
|
+
|
|
1968
|
+
type NavItem = { icon: ReactElement; title: string; url: string; };
|
|
1969
|
+
|
|
1970
|
+
export const MenuAside: React.FC = () => {
|
|
1971
|
+
const { menuActive, setMenuActive } = useMenu();
|
|
1972
|
+
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null);
|
|
1973
|
+
|
|
1974
|
+
const navItems: NavItem[] = [
|
|
1975
|
+
{ icon: <HouseIcon size={24} />, title: "Manager", url: "/manager" },
|
|
1976
|
+
{ icon: <FileEditIcon size={24} />, title: "Posts", url: "/posts" },
|
|
1977
|
+
{ icon: <FolderTreeIcon size={24} />, title: "Categorias", url: "/categories" },
|
|
1978
|
+
{ icon: <MessageCircleIcon size={24} />, title: "Mensagens", url: "/messages" },
|
|
1979
|
+
{ icon: <UserIcon size={24} />, title: "Portifolio", url: "/portifolios" },
|
|
1980
|
+
{ icon: <SettingsIcon size={24} />, title: "Setting", url: "/settings" },
|
|
1981
|
+
];
|
|
1982
|
+
|
|
1983
|
+
return (
|
|
1984
|
+
<aside className={\`\${menuActive ? "w-20" : "w-72"} h-full bg-[#202020] border-r-8 border-[#0489a1] flex flex-col transition-all duration-100 overflow-hidden z-10\`}>
|
|
1985
|
+
{/* Bot\xE3o toggle */}
|
|
1986
|
+
<button type="button" onClick={() => setMenuActive(!menuActive)} className="w-14 h-14 flex justify-center items-center text-white hover:bg-[#0489a1] transition-colors duration-300 mt-3 rounded-lg">
|
|
1987
|
+
<AlignJustifyIcon size={28} />
|
|
1988
|
+
</button>
|
|
1989
|
+
|
|
1990
|
+
{/* Navega\xE7\xE3o */}
|
|
1991
|
+
<ul className="flex flex-col mt-4 gap-2 flex-1">
|
|
1992
|
+
{navItems.map((item: NavItem, index) => (
|
|
1993
|
+
<li key={index} onMouseEnter={() => setHoveredIndex(index)} onMouseLeave={() => setHoveredIndex(null)} className={\`relative rounded-l-3xl transition-all duration-300 \${hoveredIndex === index ? "hover:bg-[#0489a1]" : ""}\`}>
|
|
1994
|
+
<Link href={item.url} className={\`flex items-center text-white py-3 transition-colors duration-200 \${hoveredIndex === index ? "text-[#0489a1] " : "text-white"}\`}>
|
|
1995
|
+
<span className="flex justify-center items-center min-w-15 h-15">
|
|
1996
|
+
{item.icon}
|
|
1997
|
+
</span>
|
|
1998
|
+
<span className={\`whitespace-nowrap text-base font-medium transition-opacity duration-300 \${menuActive ? "opacity-0 pointer-events-none" : "opacity-100"}\`}>
|
|
1999
|
+
{item.title}
|
|
2000
|
+
</span>
|
|
2001
|
+
</Link>
|
|
2002
|
+
</li>
|
|
2003
|
+
))}
|
|
2004
|
+
</ul>
|
|
2005
|
+
</aside>
|
|
2006
|
+
);
|
|
2007
|
+
};`;
|
|
2008
|
+
|
|
2009
|
+
// src/templates/components/header-comp.ts
|
|
2010
|
+
var headerTemplate = () => `
|
|
2011
|
+
"use client";
|
|
2012
|
+
|
|
2013
|
+
import { useEffect, useRef, useState } from "react";
|
|
2014
|
+
import { Menu, X } from "lucide-react";
|
|
2015
|
+
// import { ToggleButtonTheme } from "../../Buttons/ToggleButtonTheme";
|
|
2016
|
+
// import { NavBar } from "../../Navbar";
|
|
2017
|
+
// import { Logo } from "../../Logo";
|
|
2018
|
+
import { FormLoginWrapper } from "@/forms/users/FormLogin/formwraper";
|
|
2019
|
+
|
|
2020
|
+
export const Header: React.FC = () => {
|
|
2021
|
+
const [open, setOpen] = useState(false);
|
|
2022
|
+
const [isMobile, setIsMobile] = useState(false);
|
|
2023
|
+
const panelRef = useRef<HTMLDivElement | null>(null);
|
|
2024
|
+
|
|
2025
|
+
useEffect(() => {
|
|
2026
|
+
const checkSize = () => setIsMobile(window.innerWidth < 1024);
|
|
2027
|
+
checkSize();
|
|
2028
|
+
window.addEventListener("resize", checkSize);
|
|
2029
|
+
return () => window.removeEventListener("resize", checkSize);
|
|
2030
|
+
}, []);
|
|
2031
|
+
|
|
2032
|
+
useEffect(() => {
|
|
2033
|
+
const onKey = (e: KeyboardEvent) => {
|
|
2034
|
+
if (e.key === "Escape") setOpen(false);
|
|
2035
|
+
};
|
|
2036
|
+
document.addEventListener("keydown", onKey);
|
|
2037
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
2038
|
+
}, []);
|
|
2039
|
+
|
|
2040
|
+
useEffect(() => {
|
|
2041
|
+
const onClick = (e: MouseEvent) => {
|
|
2042
|
+
if (!open) return;
|
|
2043
|
+
if (panelRef.current && !panelRef.current.contains(e.target as Node)) {
|
|
2044
|
+
setOpen(false);
|
|
2045
|
+
}
|
|
2046
|
+
};
|
|
2047
|
+
document.addEventListener("mousedown", onClick);
|
|
2048
|
+
return () => document.removeEventListener("mousedown", onClick);
|
|
2049
|
+
}, [open]);
|
|
2050
|
+
|
|
2051
|
+
return (
|
|
2052
|
+
<header className="w-full px-5 py-1.5 bg-(--bg-header-color) text-(--text-color) shadow-[rgba(0,0,0,0.25)_0px_54px_55px,rgba(0,0,0,0.12)_0px_-12px_30px,rgba(0,0,0,0.12)_0px_4px_6px,rgba(0,0,0,0.17)_0px_12px_13px,rgba(0,0,0,0.09)_0px_-3px_5px] relative z-40">
|
|
2053
|
+
<div className="flex items-center justify-between lg:py-2">
|
|
2054
|
+
{/* Logo */}
|
|
2055
|
+
<div className="flex items-center px-[5%] mr-auto">
|
|
2056
|
+
{/* <Logo /> */}
|
|
2057
|
+
</div>
|
|
2058
|
+
|
|
2059
|
+
<div className="flex items-center justify-between gap-5">
|
|
2060
|
+
{/* Menu Desktop */}
|
|
2061
|
+
<nav className="hidden lg:block">
|
|
2062
|
+
{/* <NavBar onLinkClick={() => setOpen(false)} /> */}
|
|
2063
|
+
</nav>
|
|
2064
|
+
|
|
2065
|
+
{/* \xC1rea Desktop: tema + login */}
|
|
2066
|
+
<div className="hidden lg:flex items-center gap-3">
|
|
2067
|
+
{/* <ToggleButtonTheme /> */}
|
|
2068
|
+
<FormLoginWrapper />
|
|
2069
|
+
</div>
|
|
2070
|
+
|
|
2071
|
+
{/* Bot\xE3o Mobile */}
|
|
2072
|
+
{isMobile && (
|
|
2073
|
+
<button className="inline-flex items-center justify-center p-1.5 rounded-lg bg-transparent border-none cursor-pointer"
|
|
2074
|
+
onClick={() => setOpen((s) => !s)} aria-expanded={open} aria-label={open ? "Fechar menu" : "Abrir menu"}>
|
|
2075
|
+
{open ? <X size={22} /> : <Menu size={22} />}
|
|
2076
|
+
</button>
|
|
2077
|
+
)}
|
|
2078
|
+
</div>
|
|
2079
|
+
</div>
|
|
2080
|
+
|
|
2081
|
+
{/* Backdrop Mobile */}
|
|
2082
|
+
{open && isMobile && (
|
|
2083
|
+
<div className="fixed inset-0 bg-black/35 z-49" onClick={() => setOpen(false)}/>
|
|
2084
|
+
)}
|
|
2085
|
+
|
|
2086
|
+
{/* Painel Mobile */}
|
|
2087
|
+
{isMobile && (
|
|
2088
|
+
<aside className={\`fixed top-0 right-0 h-screen w-[85%] max-w-90 bg-(--bg-color) shadow-[-8px_0_30px_rgba(0,0,0,0.15)] z-50 overflow-y-auto transition-all duration-300 ease-in-out \${open ? "translate-x-0" : "translate-x-full"}\`}
|
|
2089
|
+
role="dialog" aria-hidden={!open} ref={panelRef}>
|
|
2090
|
+
<div className="flex flex-col gap-5 p-5 min-h-screen">
|
|
2091
|
+
{/* <NavBar isMobile onLinkClick={() => setOpen(false)} /> */}
|
|
2092
|
+
<div className="mt-[10%] flex flex-col justify-center items-center">
|
|
2093
|
+
{/* <ToggleButtonTheme /> */}
|
|
2094
|
+
<FormLoginWrapper />
|
|
2095
|
+
</div>
|
|
2096
|
+
</div>
|
|
2097
|
+
</aside>
|
|
2098
|
+
)}
|
|
2099
|
+
</header>
|
|
2100
|
+
);
|
|
2101
|
+
};
|
|
2102
|
+
`;
|
|
2103
|
+
|
|
2104
|
+
// src/templates/components/button-comp.ts
|
|
2105
|
+
var buttonGenericTemplate = () => `
|
|
2106
|
+
"use client";
|
|
2107
|
+
|
|
2108
|
+
import React from "react";
|
|
2109
|
+
|
|
2110
|
+
type ButtonGenericProps = {
|
|
2111
|
+
label: string;
|
|
2112
|
+
type?: "reset" | "submit" | "button";
|
|
2113
|
+
} & React.ComponentProps<"button">;
|
|
2114
|
+
|
|
2115
|
+
export const ButtonGeneric: React.FC<ButtonGenericProps> = ({
|
|
2116
|
+
label,
|
|
2117
|
+
type = "submit",
|
|
2118
|
+
className = "",
|
|
2119
|
+
...props
|
|
2120
|
+
}) => {
|
|
2121
|
+
return (
|
|
2122
|
+
<button
|
|
2123
|
+
type={type}
|
|
2124
|
+
className={\`max-h-7 px-6 py-1 bg-(--brand-300) text-white font-bold text-md rounded-md flex items-center justify-center cursor-pointer transition-colors duration-200 ease-in-out hover:bg-(--brand-400) lg:w-auto w-full lg:mt-0 \${className}\`}
|
|
2125
|
+
{...props}
|
|
2126
|
+
>
|
|
2127
|
+
{label}
|
|
2128
|
+
</button>
|
|
2129
|
+
);
|
|
2130
|
+
};
|
|
2131
|
+
`;
|
|
2132
|
+
var buttonGenericTemplateUnitTest = () => `
|
|
2133
|
+
|
|
2134
|
+
`;
|
|
2135
|
+
|
|
2136
|
+
// src/builders/resource-form-builder.ts
|
|
2137
|
+
var NextResourceFormBuilder = class extends NextAccelerateBuilder {
|
|
2138
|
+
constructor(inputName, options) {
|
|
2139
|
+
super(inputName, options);
|
|
2140
|
+
}
|
|
2141
|
+
installDependencesRequired() {
|
|
2142
|
+
DependencyFormInstaller.getInstance().install();
|
|
2143
|
+
return this;
|
|
2144
|
+
}
|
|
2145
|
+
setBasePathForForm() {
|
|
2146
|
+
this.basePath = path5.join(process.cwd(), "src/forms");
|
|
2147
|
+
createDir(this.basePath);
|
|
2148
|
+
return this;
|
|
2149
|
+
}
|
|
2150
|
+
setBasePathForComponents() {
|
|
2151
|
+
this.basePath = path5.join(process.cwd(), "src/components");
|
|
2152
|
+
createDir(this.basePath);
|
|
2153
|
+
return this;
|
|
2154
|
+
}
|
|
2155
|
+
createButtonComponentForUseInForm() {
|
|
2156
|
+
this.basePath = path5.join(process.cwd(), "src/components/ButtonGeneric");
|
|
2157
|
+
createDir(this.basePath);
|
|
2158
|
+
createFile(path5.join(this.basePath, "index.tsx"), buttonGenericTemplate());
|
|
2159
|
+
if (this.options?.test) createFile(path5.join(this.basePath, "ButtonGeneric.spec.tsx"), buttonGenericTemplateUnitTest());
|
|
2160
|
+
return this;
|
|
2161
|
+
}
|
|
2162
|
+
createCrudForm() {
|
|
2163
|
+
this.basePath = path5.join(process.cwd(), "src/forms");
|
|
2164
|
+
const sharedPath = path5.join(this.basePath, "shared");
|
|
2165
|
+
const deletePath = path5.join(sharedPath, "FormDelete");
|
|
2166
|
+
if (!pathExists(deletePath)) {
|
|
2167
|
+
createDir(sharedPath);
|
|
2168
|
+
createDir(deletePath);
|
|
2169
|
+
createFile(path5.join(deletePath, "index.tsx"), formDeleteTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
2170
|
+
createFile(path5.join(sharedPath, "index.ts"), `export { FormDeleteResource } from "./FormDelete";
|
|
2171
|
+
`);
|
|
2172
|
+
}
|
|
2173
|
+
const resourcePath = path5.join(this.basePath, this.resource);
|
|
2174
|
+
createDir(resourcePath);
|
|
2175
|
+
const formNewPath = path5.join(resourcePath, "FormNew");
|
|
2176
|
+
createDir(formNewPath);
|
|
2177
|
+
createFile(path5.join(formNewPath, "index.tsx"), formCreateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
2178
|
+
createFile(path5.join(formNewPath, "form-scheme.ts"), formSchemeCreateTemplate());
|
|
2179
|
+
const formEditPath = path5.join(resourcePath, "FormEdit");
|
|
2180
|
+
createDir(formEditPath);
|
|
2181
|
+
createFile(path5.join(formEditPath, "index.tsx"), formUpdateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
2182
|
+
createFile(path5.join(formEditPath, "form-scheme.ts"), formSchemeUpdateTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
2183
|
+
createFile(path5.join(resourcePath, "index.ts"), `export { FormNew${capitalize(this.singular)} } from "./FormNew";
|
|
2184
|
+
export { FormEdit${capitalize(this.singular)} } from "./FormEdit";`);
|
|
2185
|
+
if (this.options?.git) this.createCommit(`feat(${this.resource}): created crud form components`);
|
|
2186
|
+
return this;
|
|
2187
|
+
}
|
|
2188
|
+
};
|
|
2189
|
+
|
|
2190
|
+
// src/commands/create-form-resource.ts
|
|
2191
|
+
function createFormForResource(inputName, options) {
|
|
2192
|
+
nextProjectGuardSimple();
|
|
2193
|
+
if (!inputName) {
|
|
2194
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mPlease provide the resource name.");
|
|
2195
|
+
process.exit(1);
|
|
2196
|
+
}
|
|
2197
|
+
const builder = new NextResourceFormBuilder(inputName, options);
|
|
2198
|
+
builder.setBasePathForForm().setBasePathForComponents().createButtonComponentForUseInForm().createCrudForm().installDependencesRequired().build();
|
|
2199
|
+
console.log(`Form for the resource "${inputName}" created \x1B[32m\u2714 Success\x1B[0m`);
|
|
2200
|
+
}
|
|
2201
|
+
|
|
2202
|
+
// src/builders/next-auth-builder.ts
|
|
2203
|
+
import path6 from "path";
|
|
2204
|
+
|
|
2205
|
+
// src/templates/forms/schems/login-form-scheme.ts
|
|
2206
|
+
var formSchemeLoginTemplate = () => `
|
|
2207
|
+
import * as yup from 'yup';
|
|
2208
|
+
|
|
2209
|
+
export const formSchema = yup.object().shape({
|
|
2210
|
+
email: yup.string().required('O email \xE9 obrigat\xF3rio').email('Digite um email v\xE1lido'),
|
|
2211
|
+
password: yup.string().required('A senha \xE9 obrigat\xF3ria').min(6, 'A senha deve ter pelo menos 6 caracteres'),
|
|
2212
|
+
});
|
|
2213
|
+
|
|
2214
|
+
export type FormSchemaType = yup.InferType<typeof formSchema>;
|
|
2215
|
+
|
|
2216
|
+
|
|
2217
|
+
`;
|
|
2218
|
+
|
|
2219
|
+
// src/templates/forms/schems/redef-form-scheme.ts
|
|
2220
|
+
var formSchemeRedefTemplate = () => `
|
|
2221
|
+
import * as yup from 'yup';
|
|
2222
|
+
|
|
2223
|
+
export const formSchema = yup.object().shape({
|
|
2224
|
+
token: yup.string().required('O c\xF3digo \xE9 obrigat\xF3rio'),
|
|
2225
|
+
password: yup.string().required('A senha \xE9 obrigat\xF3ria').min(8, 'A senha deve ter pelo menos 8 caracteres'),
|
|
2226
|
+
confirpassword: yup.string().required('A confirma\xE7\xE3o de senha \xE9 obrigat\xF3ria').oneOf([yup.ref('password')], 'As senhas devem ser iguais'),
|
|
2227
|
+
});
|
|
2228
|
+
|
|
2229
|
+
export type FormSchemaType = yup.InferType<typeof formSchema>;
|
|
2230
|
+
|
|
2231
|
+
`;
|
|
2232
|
+
|
|
2233
|
+
// src/templates/forms/schems/register-form-scheme.ts
|
|
2234
|
+
var formSchemeRegisterTemplate = () => `
|
|
2235
|
+
import * as yup from "yup";
|
|
2236
|
+
|
|
2237
|
+
export const formSchema = yup.object({
|
|
2238
|
+
name: yup.string().required("O nome \xE9 obrigat\xF3rio").min(3, "O nome deve ter pelo menos 3 caracteres"),
|
|
2239
|
+
email: yup.string().required("O email \xE9 obrigat\xF3rio").email("Digite um email v\xE1lido"),
|
|
2240
|
+
cpf: yup.string().required("O CPF \xE9 obrigat\xF3rio").matches(/^d{11}$/, "O CPF deve conter 11 d\xEDgitos"),
|
|
2241
|
+
dateOfBirth: yup.date().required("A data \xE9 obrigat\xF3ria").max(new Date(), "A data de aniversario \xE9 obrigatoria"),
|
|
2242
|
+
password: yup.string().required("A senha \xE9 obrigat\xF3ria").min(6, "A senha deve ter pelo menos 6 caracteres"),
|
|
2243
|
+
repeatPassword: yup.string().required("A confirma\xE7\xE3o de senha \xE9 obrigat\xF3ria").oneOf([yup.ref("password")], "As senhas devem ser iguais"),
|
|
2244
|
+
phone: yup.string().required("O telefone \xE9 obrigat\xF3rio").matches(/^+?d{10,15}$/, "N\xFAmero de telefone inv\xE1lido"),
|
|
2245
|
+
country: yup.string().required("O pa\xEDs \xE9 obrigat\xF3rio"),
|
|
2246
|
+
state: yup.string().required("O estado \xE9 obrigat\xF3rio"),
|
|
2247
|
+
city: yup.string().required("A cidade \xE9 obrigat\xF3ria"),
|
|
2248
|
+
address: yup.string().required("O endere\xE7o \xE9 obrigat\xF3rio"),
|
|
2249
|
+
});
|
|
2250
|
+
|
|
2251
|
+
export type FormSchemaType = yup.InferType<typeof formSchema>;
|
|
2252
|
+
|
|
2253
|
+
`;
|
|
2254
|
+
|
|
2255
|
+
// src/templates/forms/schems/updateuser-form-scheme.ts
|
|
2256
|
+
var formSchemeUpdateUserTemplate = () => `
|
|
2257
|
+
import * as yup from "yup";
|
|
2258
|
+
|
|
2259
|
+
export const updateFormSchema = yup.object({
|
|
2260
|
+
photo: yup.string().url("Digite uma URL v\xE1lida para a foto").nullable().optional(),
|
|
2261
|
+
bio: yup.string().max(500, "A bio deve ter no m\xE1ximo 500 caracteres").nullable().optional(),
|
|
2262
|
+
skills: yup.string().max(100, "").nullable().optional(),
|
|
2263
|
+
hourly_rate: yup.number().min(0, "O valor deve ser maior ou igual a 0").nullable().optional(),
|
|
2264
|
+
});
|
|
2265
|
+
|
|
2266
|
+
export type UpdateFormSchemaType = yup.InferType<typeof updateFormSchema>;
|
|
2267
|
+
|
|
2268
|
+
`;
|
|
2269
|
+
|
|
2270
|
+
// src/templates/config/hiddenpaths.ts
|
|
2271
|
+
var hiddenPathsTemplate = () => `
|
|
2272
|
+
export const hiddenPaths = ["/manager", "/posts/new", "/posts", "/categories", "/categories/new", "/settings"];
|
|
2273
|
+
`;
|
|
2274
|
+
|
|
2275
|
+
// src/templates/config/proxy.ts
|
|
2276
|
+
var proxyTemplate = () => `
|
|
2277
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2278
|
+
import { decoderTokenToClaims } from "./app/api/auth/decode-claims";
|
|
2279
|
+
|
|
2280
|
+
type Role = "ADMIN" | "AUTHOR" | "COMMENTATOR";
|
|
2281
|
+
|
|
2282
|
+
const adminRoutes = ["/admin", "/users", "/dashboard/admin", "/settings", "/desk/view"];
|
|
2283
|
+
const authorRoutes = ["/desk/view"];
|
|
2284
|
+
const commntatorRoutes = ["/community", "/messages", "/", "/announced"];
|
|
2285
|
+
|
|
2286
|
+
export const config = {
|
|
2287
|
+
matcher: [
|
|
2288
|
+
"/admin/:path*", "/users/:path*", "/settings", "/settings/:path*", "/desk/view", "/desk/view/:path*", "/messages", "/messages/:path*"
|
|
2289
|
+
]
|
|
2290
|
+
};
|
|
2291
|
+
|
|
2292
|
+
function canAccessRoute(path: string, role: Role) {
|
|
2293
|
+
switch (role) {
|
|
2294
|
+
case "ADMIN":
|
|
2295
|
+
return (adminRoutes.some(r => path.startsWith(r)) || authorRoutes.some(r => path.startsWith(r)) || commntatorRoutes.some(r => path.startsWith(r)));
|
|
2296
|
+
|
|
2297
|
+
case "AUTHOR":
|
|
2298
|
+
return authorRoutes.some(r => path.startsWith(r));
|
|
2299
|
+
|
|
2300
|
+
case "COMMENTATOR":
|
|
2301
|
+
return commntatorRoutes.some(r => path.startsWith(r));
|
|
2302
|
+
|
|
2303
|
+
default:
|
|
2304
|
+
return false;
|
|
2305
|
+
};
|
|
2306
|
+
};
|
|
2307
|
+
|
|
2308
|
+
export function proxy(request: NextRequest) {
|
|
2309
|
+
const path = request.nextUrl.pathname;
|
|
2310
|
+
|
|
2311
|
+
const token = request.cookies.get("jwt_back")?.value;
|
|
2312
|
+
if (!token) {
|
|
2313
|
+
return NextResponse.redirect(new URL("/manager?error=not_has_token", request.url));
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
const claims = decoderTokenToClaims(token);
|
|
2317
|
+
if (!claims) {
|
|
2318
|
+
return NextResponse.redirect(new URL("/manager?error=invalid_token", request.url));
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
const role = claims.roles as Role;
|
|
2322
|
+
const hasAccess = canAccessRoute(path, role);
|
|
2323
|
+
|
|
2324
|
+
if (!hasAccess) {
|
|
2325
|
+
return NextResponse.redirect(new URL("/manager?error=invalid_role", request.url));
|
|
2326
|
+
}
|
|
2327
|
+
|
|
2328
|
+
return NextResponse.next();
|
|
2329
|
+
}
|
|
2330
|
+
`;
|
|
2331
|
+
|
|
2332
|
+
// src/templates/layouts/capture-error-layout.ts
|
|
2333
|
+
var captureErrorLayoutTemplate = () => `
|
|
2334
|
+
"use client";
|
|
2335
|
+
|
|
2336
|
+
import { useSearchParams } from "next/navigation";
|
|
2337
|
+
import { type ReactNode } from "react";
|
|
2338
|
+
import { toast } from "react-toastify";
|
|
2339
|
+
import { useEffect } from "react";
|
|
2340
|
+
|
|
2341
|
+
type LayoutCaptureErrorProps = { children: ReactNode; };
|
|
2342
|
+
|
|
2343
|
+
export const LayoutCaptureError: React.FC<LayoutCaptureErrorProps> = ({ children }) => {
|
|
2344
|
+
const searchParams = useSearchParams();
|
|
2345
|
+
const error = searchParams.get("error");
|
|
2346
|
+
|
|
2347
|
+
useEffect(() => {
|
|
2348
|
+
if (error) {
|
|
2349
|
+
switch (error) {
|
|
2350
|
+
case "not_has_token":
|
|
2351
|
+
toast.error("Voc\xEA n\xE3o deveria nem estar aqui bixo, est\xE1 sem token de acesso.");
|
|
2352
|
+
break;
|
|
2353
|
+
case "invalid_token":
|
|
2354
|
+
toast.error("Token inv\xE1lido, \xE9 de 1500 A.C");
|
|
2355
|
+
break;
|
|
2356
|
+
case "invalid_role":
|
|
2357
|
+
toast.error("Voc\xEA n\xE3o \xE9 um usuario do tipo que pode mecher nisso a\xED major...");
|
|
2358
|
+
break;
|
|
2359
|
+
default:
|
|
2360
|
+
toast.error("Ocorreu um erro desconhecido.");
|
|
2361
|
+
}
|
|
2362
|
+
}
|
|
2363
|
+
}, [error]);
|
|
2364
|
+
return (
|
|
2365
|
+
<div style={{ width: "100%", height: "100%" }}>
|
|
2366
|
+
{children}
|
|
2367
|
+
</div>
|
|
2368
|
+
);
|
|
2369
|
+
};
|
|
2370
|
+
`;
|
|
2371
|
+
|
|
2372
|
+
// src/templates/layouts/main-layout.ts
|
|
2373
|
+
var mainLayoutTemplate = () => `
|
|
2374
|
+
import type { ReactNode } from "react";
|
|
2375
|
+
|
|
2376
|
+
type TypeProps = { children: ReactNode[] };
|
|
2377
|
+
|
|
2378
|
+
export const MainLayout = ({ children }: TypeProps) => {
|
|
2379
|
+
const childrenArray = Array.isArray(children) ? children : [children];
|
|
2380
|
+
|
|
2381
|
+
if (childrenArray.length > 4)
|
|
2382
|
+
throw new Error(
|
|
2383
|
+
"MainLayout s\xF3 aceita no m\xE1ximo 4 elementos filhos: header, toast, main e footer."
|
|
2384
|
+
);
|
|
2385
|
+
|
|
2386
|
+
return (
|
|
2387
|
+
<div className="grid min-h-screen grid-rows-[auto_auto_1fr_auto]">
|
|
2388
|
+
<header className="max-h-50">{childrenArray[0]}</header>
|
|
2389
|
+
<div className="min-h-2.5 overflow-hidden bg-(--bg-section-100)">{childrenArray[1]}</div>
|
|
2390
|
+
<main className="min-h-[60vh]">{childrenArray[2]}</main>
|
|
2391
|
+
<footer className="h-87.5">{childrenArray[3]}</footer>
|
|
2392
|
+
</div>
|
|
2393
|
+
);
|
|
2394
|
+
};
|
|
2395
|
+
`;
|
|
2396
|
+
|
|
2397
|
+
// src/templates/layouts/manager-layout.ts
|
|
2398
|
+
var managerLayoutTemplate = () => `
|
|
2399
|
+
"use client";
|
|
2400
|
+
|
|
2401
|
+
import clsx from "clsx";
|
|
2402
|
+
import styles from "./grid.module.css";
|
|
2403
|
+
import { useMenu } from "@/contexts/manager-context";
|
|
2404
|
+
import { ReactNode } from "react";
|
|
2405
|
+
|
|
2406
|
+
type ManagerLayoutProps = {
|
|
2407
|
+
/** [0] header [1] toast,
|
|
2408
|
+
* [2] sidebar, [3] conte\xFAdo,
|
|
2409
|
+
* [4] footer
|
|
2410
|
+
**/
|
|
2411
|
+
children: [ReactNode, ReactNode, ReactNode, ReactNode, ReactNode];
|
|
2412
|
+
};
|
|
2413
|
+
|
|
2414
|
+
export const ManagerLayout: React.FC<ManagerLayoutProps> = ({ children }) => {
|
|
2415
|
+
const [header, toastify, sidebar, content, footer] = children;
|
|
2416
|
+
const { menuActive } = useMenu();
|
|
2417
|
+
|
|
2418
|
+
return (
|
|
2419
|
+
<div className={clsx(styles.layout)} style={{ ["--aside-width" as any]: menuActive ? "80px" : "220px" }}>
|
|
2420
|
+
<header className={clsx(styles.header)}>{header}</header>
|
|
2421
|
+
<div className={clsx(styles.toastify)}>{toastify}</div>
|
|
2422
|
+
|
|
2423
|
+
<aside className={clsx(styles.side)}>{sidebar}</aside>
|
|
2424
|
+
<main className={clsx(styles.content)}>{content}</main>
|
|
2425
|
+
|
|
2426
|
+
<footer className={clsx(styles.footer)}>{footer}</footer>
|
|
2427
|
+
</div>
|
|
2428
|
+
);
|
|
2429
|
+
};
|
|
2430
|
+
|
|
2431
|
+
`;
|
|
2432
|
+
|
|
2433
|
+
// src/templates/layouts/manager-layout-css.ts
|
|
2434
|
+
var managerLayoutCssTemplate = () => `
|
|
2435
|
+
.layout {
|
|
2436
|
+
width: 100%;
|
|
2437
|
+
display: grid;
|
|
2438
|
+
grid-template-columns: var(--aside-width, 220px) 1fr;
|
|
2439
|
+
grid-template-rows: auto auto auto auto;
|
|
2440
|
+
grid-template-areas:
|
|
2441
|
+
"header header"
|
|
2442
|
+
"toast toast"
|
|
2443
|
+
"side content"
|
|
2444
|
+
"footer footer";
|
|
2445
|
+
transition: grid-template-columns 0.3s ease;
|
|
2446
|
+
}
|
|
2447
|
+
|
|
2448
|
+
.header {
|
|
2449
|
+
grid-area: header;
|
|
2450
|
+
}
|
|
2451
|
+
|
|
2452
|
+
.toastify {
|
|
2453
|
+
grid-area: toast;
|
|
2454
|
+
min-height: 2px;
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
.content {
|
|
2458
|
+
grid-area: content;
|
|
2459
|
+
min-height: 0;
|
|
2460
|
+
overflow-y: auto;
|
|
2461
|
+
}
|
|
2462
|
+
|
|
2463
|
+
.side {
|
|
2464
|
+
grid-area: side;
|
|
2465
|
+
min-width: 80px;
|
|
2466
|
+
overflow: hidden;
|
|
2467
|
+
}
|
|
2468
|
+
|
|
2469
|
+
.footer {
|
|
2470
|
+
grid-area: footer;
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
@media (max-width: 768px) {
|
|
2474
|
+
.layout {
|
|
2475
|
+
grid-template-columns: 60px 1fr;
|
|
2476
|
+
}
|
|
2477
|
+
}`;
|
|
2478
|
+
|
|
2479
|
+
// src/templates/layouts/private-next-layout.ts
|
|
2480
|
+
var privateNextLayoutTemplate = () => `
|
|
2481
|
+
import { Footer } from "@/components/Footer";
|
|
2482
|
+
import { Header } from "@/components/Header";
|
|
2483
|
+
import { LayoutCaptureError } from "@/components/Layouts/LayoutCaptureError";
|
|
2484
|
+
import { ManagerLayout } from "@/components/Layouts/ManagerLayout";
|
|
2485
|
+
import { MenuAside } from "@/components/MenuAside";
|
|
2486
|
+
import { MenuProvider } from "@/contexts/manager-context";
|
|
2487
|
+
import { ToastContainer } from "react-toastify";
|
|
2488
|
+
import { Suspense } from "react";
|
|
2489
|
+
|
|
2490
|
+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
|
|
2491
|
+
return (
|
|
2492
|
+
<div data-testid="root-layout-private">
|
|
2493
|
+
<MenuProvider>
|
|
2494
|
+
<ManagerLayout>
|
|
2495
|
+
<Header />
|
|
2496
|
+
<ToastContainer position="top-center" />
|
|
2497
|
+
<MenuAside />
|
|
2498
|
+
<Suspense fallback={null}>
|
|
2499
|
+
<LayoutCaptureError>
|
|
2500
|
+
{children}
|
|
2501
|
+
</LayoutCaptureError>
|
|
2502
|
+
</Suspense>
|
|
2503
|
+
<Footer />
|
|
2504
|
+
</ManagerLayout>
|
|
2505
|
+
</MenuProvider>
|
|
2506
|
+
</div>
|
|
2507
|
+
);
|
|
2508
|
+
}
|
|
2509
|
+
`;
|
|
2510
|
+
|
|
2511
|
+
// src/templates/layouts/root-next-layout.ts
|
|
2512
|
+
var rootLayoutMinimalTemplate = () => `
|
|
2513
|
+
import type { Metadata } from "next";
|
|
2514
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
2515
|
+
import "./globals.css";
|
|
2516
|
+
|
|
2517
|
+
const geistSans = Geist({
|
|
2518
|
+
variable: "--font-geist-sans",
|
|
2519
|
+
subsets: ["latin"],
|
|
2520
|
+
});
|
|
2521
|
+
|
|
2522
|
+
const geistMono = Geist_Mono({
|
|
2523
|
+
variable: "--font-geist-mono",
|
|
2524
|
+
subsets: ["latin"],
|
|
2525
|
+
});
|
|
2526
|
+
|
|
2527
|
+
export const metadata: Metadata = {
|
|
2528
|
+
title: "Create Next App",
|
|
2529
|
+
description: "Generated by create next app",
|
|
2530
|
+
};
|
|
2531
|
+
|
|
2532
|
+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
|
|
2533
|
+
return (
|
|
2534
|
+
<html lang="en">
|
|
2535
|
+
<body className={\`\${geistSans.variable} \${geistMono.variable} antialiased\`} data-testid="root-layout">
|
|
2536
|
+
{children}
|
|
2537
|
+
</body>
|
|
2538
|
+
</html>
|
|
2539
|
+
);
|
|
2540
|
+
}
|
|
2541
|
+
`;
|
|
2542
|
+
|
|
2543
|
+
// src/templates/layouts/public-next-layout.ts
|
|
2544
|
+
var publicLayoutTemplate = () => `
|
|
2545
|
+
import { Footer } from "@/components/Footer";
|
|
2546
|
+
import { Header } from "@/components/Header";
|
|
2547
|
+
import { MainLayout } from "@/components/Layouts/MainLayout";
|
|
2548
|
+
import { ToastContainer } from "react-toastify";
|
|
2549
|
+
|
|
2550
|
+
export default function RootLayout({ children }: Readonly<{ children: React.ReactNode; }>) {
|
|
2551
|
+
return (
|
|
2552
|
+
<div data-testid="root-layout-public">
|
|
2553
|
+
<MainLayout>
|
|
2554
|
+
<Header />
|
|
2555
|
+
<ToastContainer position="top-center" />
|
|
2556
|
+
{children}
|
|
2557
|
+
<Footer />
|
|
2558
|
+
</MainLayout>
|
|
2559
|
+
</div>
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
|
|
2563
|
+
`;
|
|
2564
|
+
|
|
2565
|
+
// src/utils/services/install-nextauth-motion.service.ts
|
|
2566
|
+
import { execSync as execSync4 } from "child_process";
|
|
2567
|
+
var DependencyFramemotionAndNextAuthInstaller = class _DependencyFramemotionAndNextAuthInstaller {
|
|
2568
|
+
constructor() {
|
|
2569
|
+
this.hasInstalled = false;
|
|
2570
|
+
}
|
|
2571
|
+
static getInstance() {
|
|
2572
|
+
if (!_DependencyFramemotionAndNextAuthInstaller.instance) {
|
|
2573
|
+
_DependencyFramemotionAndNextAuthInstaller.instance = new _DependencyFramemotionAndNextAuthInstaller();
|
|
2574
|
+
}
|
|
2575
|
+
return _DependencyFramemotionAndNextAuthInstaller.instance;
|
|
2576
|
+
}
|
|
2577
|
+
async install() {
|
|
2578
|
+
if (this.hasInstalled || hasDependency("framer-motion")) return;
|
|
2579
|
+
this.hasInstalled = true;
|
|
2580
|
+
try {
|
|
2581
|
+
execSync4(`npm install framer-motion &&
|
|
2582
|
+
npm install next-auth &&
|
|
2583
|
+
npm install jwt-decode &&
|
|
2584
|
+
npm install --save-dev @types/jwt-decode
|
|
2585
|
+
`, { stdio: "inherit" });
|
|
2586
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
2587
|
+
} catch (error) {
|
|
2588
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
2589
|
+
}
|
|
2590
|
+
;
|
|
2591
|
+
}
|
|
2592
|
+
};
|
|
2593
|
+
|
|
2594
|
+
// src/builders/next-auth-builder.ts
|
|
2595
|
+
var NextAuthBuilder = class {
|
|
2596
|
+
constructor(options) {
|
|
2597
|
+
this.options = options;
|
|
2598
|
+
}
|
|
2599
|
+
createCommit(message) {
|
|
2600
|
+
if (!this.options?.git) return;
|
|
2601
|
+
gitCommit(message);
|
|
2602
|
+
}
|
|
2603
|
+
installDependencesRequired() {
|
|
2604
|
+
DependencyFramemotionAndNextAuthInstaller.getInstance().install();
|
|
2605
|
+
return this;
|
|
2606
|
+
}
|
|
2607
|
+
setBasePathAndCreateConfig() {
|
|
2608
|
+
this.basePath = path6.join(process.cwd(), "src/app/api/auth/[...nextauth]");
|
|
2609
|
+
createDir(this.basePath);
|
|
2610
|
+
if (!pathExists(this.basePath)) {
|
|
2611
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0m creating NextAuth config directory.");
|
|
2612
|
+
process.exit(1);
|
|
2613
|
+
}
|
|
2614
|
+
;
|
|
2615
|
+
createFile(path6.join(this.basePath, "route.ts"), nextConfigTemplate());
|
|
2616
|
+
if (this.options?.git) this.createCommit("feat(next-auth): create config route.");
|
|
2617
|
+
return this;
|
|
2618
|
+
}
|
|
2619
|
+
createNextAuthAuxOptions() {
|
|
2620
|
+
this.basePath = path6.join(process.cwd(), "src/app/api/auth/");
|
|
2621
|
+
createFile(path6.join(this.basePath, "decode-claims.ts"), nextDecodeClaimsTemplate());
|
|
2622
|
+
createFile(path6.join(this.basePath, "request-api.ts"), nextRequestApiTemplate());
|
|
2623
|
+
this.basePath = path6.join(process.cwd(), "src/utils");
|
|
2624
|
+
createDir(this.basePath);
|
|
2625
|
+
if (!pathExists(this.basePath)) {
|
|
2626
|
+
throw new Error(`Failed to create directory at ${this.basePath}`);
|
|
2627
|
+
}
|
|
2628
|
+
;
|
|
2629
|
+
createFile(path6.join(this.basePath, "route.ts"), nextSessionTypeTemplate());
|
|
2630
|
+
if (!pathExists(this.basePath)) {
|
|
2631
|
+
throw new Error(`Failed to create directory at ${this.basePath}`);
|
|
2632
|
+
}
|
|
2633
|
+
;
|
|
2634
|
+
createFile(path6.join(this.basePath, "utils.ts"), utilsTypeTemplate());
|
|
2635
|
+
if (this.options?.git) this.createCommit("feat(next-auth): create aux options for login sessions.");
|
|
2636
|
+
return this;
|
|
2637
|
+
}
|
|
2638
|
+
createNextAuthForms() {
|
|
2639
|
+
const forms = [
|
|
2640
|
+
{ name: "FormForgot", template: formForgotTemplate, scheme: () => "" },
|
|
2641
|
+
{ name: "FormLogin", template: formLoginTemplate, scheme: formSchemeLoginTemplate },
|
|
2642
|
+
{ name: "FormRedef", template: formRedefTemplate, scheme: formSchemeRedefTemplate },
|
|
2643
|
+
{ name: "FormRegister", template: formRegisterTemplate, scheme: formSchemeRegisterTemplate },
|
|
2644
|
+
{ name: "FormUpdateUser", template: formUpdateUserTemplate, scheme: formSchemeUpdateUserTemplate }
|
|
2645
|
+
];
|
|
2646
|
+
const formsRoot = path6.join(process.cwd(), "src/forms");
|
|
2647
|
+
const usersPath = path6.join(formsRoot, "users");
|
|
2648
|
+
let created = false;
|
|
2649
|
+
if (!pathExists(formsRoot)) {
|
|
2650
|
+
createDir(formsRoot);
|
|
2651
|
+
created = true;
|
|
2652
|
+
}
|
|
2653
|
+
;
|
|
2654
|
+
if (!pathExists(usersPath)) {
|
|
2655
|
+
createDir(usersPath);
|
|
2656
|
+
created = true;
|
|
2657
|
+
}
|
|
2658
|
+
;
|
|
2659
|
+
forms.forEach(({ name, template, scheme }) => {
|
|
2660
|
+
const formDir = path6.join(usersPath, name);
|
|
2661
|
+
if (!pathExists(formDir)) {
|
|
2662
|
+
createDir(formDir);
|
|
2663
|
+
created = true;
|
|
2664
|
+
}
|
|
2665
|
+
;
|
|
2666
|
+
const filePath = path6.join(formDir, "index.tsx");
|
|
2667
|
+
if (!pathExists(filePath)) {
|
|
2668
|
+
createFile(filePath, template());
|
|
2669
|
+
created = true;
|
|
2670
|
+
}
|
|
2671
|
+
;
|
|
2672
|
+
const schemePath = path6.join(formDir, `${name.toLocaleLowerCase() + "-scheme"}.tsx`);
|
|
2673
|
+
if (!pathExists(schemePath)) {
|
|
2674
|
+
createFile(schemePath, scheme());
|
|
2675
|
+
created = true;
|
|
2676
|
+
}
|
|
2677
|
+
;
|
|
2678
|
+
});
|
|
2679
|
+
this.basePath = path6.join(process.cwd(), "src/forms/users/FormLogin");
|
|
2680
|
+
createFile(path6.join(this.basePath, "formwraper.tsx"), formLoginWrapperTemplate());
|
|
2681
|
+
createFile(path6.join(this.basePath, "hidenpath.ts"), hiddenPathsTemplate());
|
|
2682
|
+
const indexPath = path6.join(usersPath, "index.ts");
|
|
2683
|
+
if (!pathExists(indexPath)) {
|
|
2684
|
+
const exports = forms.map(({ name }) => `export { ${name} } from "./${name}";`).join("\n");
|
|
2685
|
+
createFile(indexPath, exports + "\n");
|
|
2686
|
+
created = true;
|
|
2687
|
+
}
|
|
2688
|
+
if (created && this.options?.git) {
|
|
2689
|
+
this.createCommit("feat(auth): create next-auth user forms");
|
|
2690
|
+
}
|
|
2691
|
+
return this;
|
|
2692
|
+
}
|
|
2693
|
+
createNextLayouts() {
|
|
2694
|
+
const layoutsRoot = path6.join(process.cwd(), "src/components/Layouts");
|
|
2695
|
+
let created = false;
|
|
2696
|
+
if (!pathExists(layoutsRoot)) {
|
|
2697
|
+
createDir(layoutsRoot);
|
|
2698
|
+
created = true;
|
|
2699
|
+
}
|
|
2700
|
+
const layouts = [
|
|
2701
|
+
{ name: "LayoutCaptureError", files: [{ name: "index.tsx", content: captureErrorLayoutTemplate() }] },
|
|
2702
|
+
{ name: "MainLayout", files: [{ name: "index.tsx", content: mainLayoutTemplate() }] },
|
|
2703
|
+
{ name: "ManagerLayout", files: [{ name: "index.tsx", content: managerLayoutTemplate() }, { name: "grid.module.css", content: managerLayoutCssTemplate() }] }
|
|
2704
|
+
];
|
|
2705
|
+
layouts.forEach((layout) => {
|
|
2706
|
+
const layoutDir = path6.join(layoutsRoot, layout.name);
|
|
2707
|
+
if (!pathExists(layoutDir)) {
|
|
2708
|
+
createDir(layoutDir);
|
|
2709
|
+
created = true;
|
|
2710
|
+
}
|
|
2711
|
+
;
|
|
2712
|
+
layout.files.forEach((file) => {
|
|
2713
|
+
const filePath = path6.join(layoutDir, file.name);
|
|
2714
|
+
if (!pathExists(filePath)) {
|
|
2715
|
+
createFile(filePath, file.content);
|
|
2716
|
+
created = true;
|
|
2717
|
+
}
|
|
2718
|
+
});
|
|
2719
|
+
});
|
|
2720
|
+
if (created && this.options?.git) this.createCommit("feat(next-auth): compoenets for authentication system.");
|
|
2721
|
+
return this;
|
|
2722
|
+
}
|
|
2723
|
+
setLayouts() {
|
|
2724
|
+
const appRoot = path6.join(process.cwd(), "src/app");
|
|
2725
|
+
const publicsDir = path6.join(appRoot, "(publics)");
|
|
2726
|
+
const privatesDir = path6.join(appRoot, "(privates)");
|
|
2727
|
+
const managerDir = path6.join(privatesDir, "manager");
|
|
2728
|
+
let created = false;
|
|
2729
|
+
if (!pathExists(publicsDir)) {
|
|
2730
|
+
createDir(publicsDir);
|
|
2731
|
+
created = true;
|
|
2732
|
+
}
|
|
2733
|
+
if (!pathExists(privatesDir)) {
|
|
2734
|
+
createDir(privatesDir);
|
|
2735
|
+
created = true;
|
|
2736
|
+
}
|
|
2737
|
+
if (!pathExists(managerDir)) {
|
|
2738
|
+
createDir(managerDir);
|
|
2739
|
+
created = true;
|
|
2740
|
+
}
|
|
2741
|
+
const rootLayout = path6.join(appRoot, "layout.tsx");
|
|
2742
|
+
if (!pathExists(rootLayout)) {
|
|
2743
|
+
createFile(rootLayout, rootLayoutMinimalTemplate());
|
|
2744
|
+
created = true;
|
|
2745
|
+
}
|
|
2746
|
+
const rootPage = path6.join(appRoot, "page.tsx");
|
|
2747
|
+
const publicsPage = path6.join(publicsDir, "page.tsx");
|
|
2748
|
+
if (pathExists(rootPage) && !pathExists(publicsPage)) {
|
|
2749
|
+
moveFile(rootPage, publicsPage);
|
|
2750
|
+
created = true;
|
|
2751
|
+
}
|
|
2752
|
+
const publicsLayout = path6.join(publicsDir, "layout.tsx");
|
|
2753
|
+
if (!pathExists(publicsLayout)) {
|
|
2754
|
+
createFile(publicsLayout, publicLayoutTemplate());
|
|
2755
|
+
created = true;
|
|
2756
|
+
}
|
|
2757
|
+
const privateLayoutPath = path6.join(privatesDir, "layout.tsx");
|
|
2758
|
+
if (!pathExists(privateLayoutPath)) {
|
|
2759
|
+
createFile(privateLayoutPath, privateNextLayoutTemplate());
|
|
2760
|
+
created = true;
|
|
2761
|
+
}
|
|
2762
|
+
const managerPagePath = path6.join(managerDir, "page.tsx");
|
|
2763
|
+
if (!pathExists(managerPagePath)) {
|
|
2764
|
+
createFile(managerPagePath, managerPageTemplate());
|
|
2765
|
+
created = true;
|
|
2766
|
+
}
|
|
2767
|
+
if (created && this.options?.git) {
|
|
2768
|
+
this.createCommit("feat(app-router): created private and publics route groups");
|
|
2769
|
+
}
|
|
2770
|
+
;
|
|
2771
|
+
return this;
|
|
2772
|
+
}
|
|
2773
|
+
createComponentsAux() {
|
|
2774
|
+
const root = process.cwd();
|
|
2775
|
+
const contextsDir = path6.join(root, "src/contexts");
|
|
2776
|
+
const componentsDir = path6.join(root, "src/components");
|
|
2777
|
+
const headerDir = path6.join(componentsDir, "Header");
|
|
2778
|
+
const footerDir = path6.join(componentsDir, "Footer");
|
|
2779
|
+
const menuAsideDir = path6.join(componentsDir, "MenuAside");
|
|
2780
|
+
const buttonGenericDir = path6.join(componentsDir, "ButtonGeneric");
|
|
2781
|
+
let created = false;
|
|
2782
|
+
if (!pathExists(contextsDir)) {
|
|
2783
|
+
createDir(contextsDir);
|
|
2784
|
+
created = true;
|
|
2785
|
+
}
|
|
2786
|
+
;
|
|
2787
|
+
const managerContextPath = path6.join(contextsDir, "manager-context.tsx");
|
|
2788
|
+
if (!pathExists(managerContextPath)) {
|
|
2789
|
+
createFile(managerContextPath, managerContextTemplate());
|
|
2790
|
+
created = true;
|
|
2791
|
+
}
|
|
2792
|
+
if (!pathExists(componentsDir)) {
|
|
2793
|
+
createDir(componentsDir);
|
|
2794
|
+
created = true;
|
|
2795
|
+
}
|
|
2796
|
+
const components = [
|
|
2797
|
+
{ dir: headerDir, template: headerTemplate() },
|
|
2798
|
+
{ dir: footerDir, template: footerTemplate() },
|
|
2799
|
+
{ dir: menuAsideDir, template: menuAsideTemplate() },
|
|
2800
|
+
{ dir: buttonGenericDir, template: buttonGenericTemplate() }
|
|
2801
|
+
];
|
|
2802
|
+
components.forEach((component) => {
|
|
2803
|
+
if (!pathExists(component.dir)) {
|
|
2804
|
+
createDir(component.dir);
|
|
2805
|
+
created = true;
|
|
2806
|
+
}
|
|
2807
|
+
const indexPath = path6.join(component.dir, "index.tsx");
|
|
2808
|
+
if (!pathExists(indexPath)) {
|
|
2809
|
+
createFile(indexPath, component.template);
|
|
2810
|
+
created = true;
|
|
2811
|
+
}
|
|
2812
|
+
});
|
|
2813
|
+
if (created && this.options?.git) {
|
|
2814
|
+
this.createCommit("feat(core): create manager context and essential layout components");
|
|
2815
|
+
}
|
|
2816
|
+
;
|
|
2817
|
+
return this;
|
|
2818
|
+
}
|
|
2819
|
+
createNextAutorizationSystem() {
|
|
2820
|
+
this.basePath = path6.join(process.cwd(), "src");
|
|
2821
|
+
createFile(path6.join(this.basePath, "proxy.ts"), proxyTemplate());
|
|
2822
|
+
if (this.options?.git) this.createCommit("feat(next-auth): create proxy for autorize users in frontend.");
|
|
2823
|
+
return this;
|
|
2824
|
+
}
|
|
2825
|
+
setEnvironmentVariable() {
|
|
2826
|
+
this.basePath = path6.join(process.cwd());
|
|
2827
|
+
createFile(path6.join(this.basePath, ".env.local"), environmentTemplate());
|
|
2828
|
+
return this;
|
|
2829
|
+
}
|
|
2830
|
+
build() {
|
|
2831
|
+
if (!this.options?.git) return;
|
|
2832
|
+
console.log("commits made successfully \u2728");
|
|
2833
|
+
}
|
|
2834
|
+
};
|
|
2835
|
+
|
|
2836
|
+
// src/commands/create-nextauth-resource.ts
|
|
2837
|
+
function createNextAutResource(options) {
|
|
2838
|
+
nextProjectGuardSimple();
|
|
2839
|
+
const builder = new NextAuthBuilder(options);
|
|
2840
|
+
builder.installDependencesRequired().setBasePathAndCreateConfig().createNextAuthAuxOptions().createNextAuthForms().createNextLayouts().setLayouts().createComponentsAux().createNextAutorizationSystem().setEnvironmentVariable().build();
|
|
2841
|
+
console.log(`next auth configured \x1B[32m\u2714 success\x1B[0m`);
|
|
2842
|
+
}
|
|
2843
|
+
|
|
2844
|
+
// src/builders/tests-builder.ts
|
|
2845
|
+
import path7 from "path";
|
|
2846
|
+
import fs4 from "fs";
|
|
2847
|
+
|
|
2848
|
+
// src/templates/tests/config-vitest.ts
|
|
2849
|
+
var nextConfigViTestTemplate = () => `
|
|
2850
|
+
/// <reference types="vitest" />
|
|
2851
|
+
import { defineConfig } from "vitest/config";
|
|
2852
|
+
import path from "path";
|
|
2853
|
+
|
|
2854
|
+
export default defineConfig({
|
|
2855
|
+
test: {
|
|
2856
|
+
environment: "jsdom",
|
|
2857
|
+
globals: true,
|
|
2858
|
+
setupFiles: "./vitest.setup.ts",
|
|
2859
|
+
coverage: {
|
|
2860
|
+
provider: "v8",
|
|
2861
|
+
reporter: ["text", "html"],
|
|
2862
|
+
},
|
|
2863
|
+
},
|
|
2864
|
+
resolve: {
|
|
2865
|
+
alias: {
|
|
2866
|
+
"@": path.resolve(__dirname, "./src"),
|
|
2867
|
+
},
|
|
2868
|
+
},
|
|
2869
|
+
});
|
|
2870
|
+
`;
|
|
2871
|
+
|
|
2872
|
+
// src/utils/services/install-tests-unit-dependences.service.ts
|
|
2873
|
+
import { execSync as execSync5 } from "child_process";
|
|
2874
|
+
var DependencyTetsUnitInstaller = class _DependencyTetsUnitInstaller {
|
|
2875
|
+
constructor() {
|
|
2876
|
+
this.hasInstalled = false;
|
|
2877
|
+
}
|
|
2878
|
+
static getInstance() {
|
|
2879
|
+
if (!_DependencyTetsUnitInstaller.instance) {
|
|
2880
|
+
_DependencyTetsUnitInstaller.instance = new _DependencyTetsUnitInstaller();
|
|
2881
|
+
}
|
|
2882
|
+
return _DependencyTetsUnitInstaller.instance;
|
|
2883
|
+
}
|
|
2884
|
+
async install() {
|
|
2885
|
+
if (this.hasInstalled || hasDependency("vitest")) return;
|
|
2886
|
+
this.hasInstalled = true;
|
|
2887
|
+
try {
|
|
2888
|
+
execSync5(`
|
|
2889
|
+
npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom @types/node @vitest/ui @vitest/coverage-v8
|
|
2890
|
+
`, { stdio: "inherit" });
|
|
2891
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
2892
|
+
} catch (error) {
|
|
2893
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
2894
|
+
}
|
|
2895
|
+
;
|
|
2896
|
+
}
|
|
2897
|
+
};
|
|
2898
|
+
|
|
2899
|
+
// src/utils/services/install-tests-e2e-dependences.service.ts
|
|
2900
|
+
import { execSync as execSync6 } from "child_process";
|
|
2901
|
+
var DependencyTetsE2EInstaller = class _DependencyTetsE2EInstaller {
|
|
2902
|
+
constructor() {
|
|
2903
|
+
this.hasInstalled = false;
|
|
2904
|
+
}
|
|
2905
|
+
static getInstance() {
|
|
2906
|
+
if (!_DependencyTetsE2EInstaller.instance) {
|
|
2907
|
+
_DependencyTetsE2EInstaller.instance = new _DependencyTetsE2EInstaller();
|
|
2908
|
+
}
|
|
2909
|
+
return _DependencyTetsE2EInstaller.instance;
|
|
2910
|
+
}
|
|
2911
|
+
async install() {
|
|
2912
|
+
if (this.hasInstalled || hasDependency("playwright")) return;
|
|
2913
|
+
this.hasInstalled = true;
|
|
2914
|
+
try {
|
|
2915
|
+
execSync6(`npm install -D @playwright/test`, { stdio: "inherit" });
|
|
2916
|
+
console.log("Depend\xEAncias instaladas com sucesso!");
|
|
2917
|
+
} catch (error) {
|
|
2918
|
+
console.error("Falha ao instalar depend\xEAncias:", error);
|
|
2919
|
+
}
|
|
2920
|
+
;
|
|
2921
|
+
}
|
|
2922
|
+
};
|
|
2923
|
+
|
|
2924
|
+
// src/templates/tests/config-playwright.ts
|
|
2925
|
+
var nextConfigPlaywightTestTemplate = () => `
|
|
2926
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
2927
|
+
|
|
2928
|
+
export default defineConfig({
|
|
2929
|
+
testDir: './tests/e2e',
|
|
2930
|
+
timeout: 30 * 1000, // 30s por teste
|
|
2931
|
+
use: {
|
|
2932
|
+
headless: true, // false para abrir browser
|
|
2933
|
+
viewport: { width: 1280, height: 720 },
|
|
2934
|
+
baseURL: 'http://localhost:3000',
|
|
2935
|
+
trace: 'on-first-retry', // \xFAtil para debugar
|
|
2936
|
+
},
|
|
2937
|
+
projects: [
|
|
2938
|
+
{
|
|
2939
|
+
name: 'Chromium',
|
|
2940
|
+
use: { ...devices['Desktop Chrome'] },
|
|
2941
|
+
},
|
|
2942
|
+
{
|
|
2943
|
+
name: 'Firefox',
|
|
2944
|
+
use: { ...devices['Desktop Firefox'] },
|
|
2945
|
+
},
|
|
2946
|
+
{
|
|
2947
|
+
name: 'WebKit',
|
|
2948
|
+
use: { ...devices['Desktop Safari'] },
|
|
2949
|
+
},
|
|
2950
|
+
],
|
|
2951
|
+
webServer: {
|
|
2952
|
+
command: 'npm run dev',
|
|
2953
|
+
port: 3000,
|
|
2954
|
+
reuseExistingServer: !process.env.CI,
|
|
2955
|
+
},
|
|
2956
|
+
});
|
|
2957
|
+
`;
|
|
2958
|
+
|
|
2959
|
+
// src/builders/tests-builder.ts
|
|
2960
|
+
var TestsBuilder = class {
|
|
2961
|
+
constructor(options) {
|
|
2962
|
+
this.options = options;
|
|
2963
|
+
}
|
|
2964
|
+
createCommit(message) {
|
|
2965
|
+
if (!this.options?.git) return;
|
|
2966
|
+
gitCommit(message);
|
|
2967
|
+
}
|
|
2968
|
+
installDependencesViTestRequired() {
|
|
2969
|
+
DependencyTetsUnitInstaller.getInstance().install();
|
|
2970
|
+
return this;
|
|
2971
|
+
}
|
|
2972
|
+
installDependencesPlaywrightTestRequired() {
|
|
2973
|
+
DependencyTetsE2EInstaller.getInstance().install();
|
|
2974
|
+
return this;
|
|
2975
|
+
}
|
|
2976
|
+
setBasePathAndCreateConfigViTest() {
|
|
2977
|
+
this.basePath = path7.join(process.cwd());
|
|
2978
|
+
const vitestConfigPath = path7.join(this.basePath, "vitest.config.ts");
|
|
2979
|
+
createFile(vitestConfigPath, nextConfigViTestTemplate());
|
|
2980
|
+
const setupPath = path7.join(this.basePath, "vitest.setup.ts");
|
|
2981
|
+
createFile(setupPath, 'import "@testing-library/jest-dom";\n');
|
|
2982
|
+
const tsconfigPath = path7.join(this.basePath, "tsconfig.json");
|
|
2983
|
+
if (pathExists(tsconfigPath)) {
|
|
2984
|
+
const tsconfigRaw = fs4.readFileSync(tsconfigPath, "utf-8");
|
|
2985
|
+
const tsconfig = JSON.parse(tsconfigRaw);
|
|
2986
|
+
tsconfig.compilerOptions = tsconfig.compilerOptions || {};
|
|
2987
|
+
tsconfig.compilerOptions.types = tsconfig.compilerOptions.types || [];
|
|
2988
|
+
if (!tsconfig.compilerOptions.types.includes("vitest/globals")) {
|
|
2989
|
+
tsconfig.compilerOptions.types.push("vitest/globals");
|
|
2990
|
+
}
|
|
2991
|
+
fs4.writeFileSync(tsconfigPath, JSON.stringify(tsconfig, null, 2));
|
|
2992
|
+
}
|
|
2993
|
+
const packageJsonPath = path7.join(this.basePath, "package.json");
|
|
2994
|
+
if (pathExists(packageJsonPath)) {
|
|
2995
|
+
const packageJsonRaw = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
2996
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
2997
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
2998
|
+
packageJson.scripts["test"] = "vitest";
|
|
2999
|
+
packageJson.scripts["test:ui"] = "vitest --ui";
|
|
3000
|
+
packageJson.scripts["test:run"] = "vitest run";
|
|
3001
|
+
packageJson.scripts["test:coverage"] = "vitest run --coverage";
|
|
3002
|
+
fs4.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3003
|
+
}
|
|
3004
|
+
if (this.options?.git) this.createCommit("chore(tests-unit): setup Vitest and testing-library");
|
|
3005
|
+
return this;
|
|
3006
|
+
}
|
|
3007
|
+
setBasePathAndCreatePlaywrightConfigTest() {
|
|
3008
|
+
this.basePath = path7.join(process.cwd());
|
|
3009
|
+
const playwrightConfigPath = path7.join(this.basePath, "playwright.config.ts");
|
|
3010
|
+
createFile(playwrightConfigPath, nextConfigPlaywightTestTemplate());
|
|
3011
|
+
const e2ePath = path7.join(this.basePath, "tests", "e2e");
|
|
3012
|
+
createDir(e2ePath);
|
|
3013
|
+
const packageJsonPath = path7.join(this.basePath, "package.json");
|
|
3014
|
+
if (pathExists(packageJsonPath)) {
|
|
3015
|
+
const packageJsonRaw = fs4.readFileSync(packageJsonPath, "utf-8");
|
|
3016
|
+
const packageJson = JSON.parse(packageJsonRaw);
|
|
3017
|
+
packageJson.scripts = packageJson.scripts || {};
|
|
3018
|
+
packageJson.scripts["e2e"] = "playwright test";
|
|
3019
|
+
packageJson.scripts["e2e:headed"] = "playwright test --headed";
|
|
3020
|
+
packageJson.scripts["e2e:report"] = "playwright show-report";
|
|
3021
|
+
fs4.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2));
|
|
3022
|
+
}
|
|
3023
|
+
if (this.options?.git) this.createCommit("chore(tests): setup Playwright E2E");
|
|
3024
|
+
return this;
|
|
3025
|
+
}
|
|
3026
|
+
build() {
|
|
3027
|
+
if (!this.options?.git) return;
|
|
3028
|
+
console.log("commits made successfully \u2728");
|
|
3029
|
+
}
|
|
3030
|
+
};
|
|
3031
|
+
|
|
3032
|
+
// src/commands/create-config-tests.ts
|
|
3033
|
+
function createTestUnitConfig(options) {
|
|
3034
|
+
nextProjectGuardSimple();
|
|
3035
|
+
const builder = new TestsBuilder(options);
|
|
3036
|
+
builder.setBasePathAndCreateConfigViTest().installDependencesViTestRequired().build();
|
|
3037
|
+
console.log(`resource configured \x1B[32m\u2714 Success\x1B[0m`);
|
|
3038
|
+
}
|
|
3039
|
+
function createTestE2EConfig(options) {
|
|
3040
|
+
nextProjectGuardSimple();
|
|
3041
|
+
const builder = new TestsBuilder(options);
|
|
3042
|
+
builder.setBasePathAndCreatePlaywrightConfigTest().installDependencesPlaywrightTestRequired().build();
|
|
3043
|
+
console.log(`resource configured \x1B[32m\u2714 Success\x1B[0m`);
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
// src/builders/resource-api-builder.ts
|
|
3047
|
+
import path8 from "path";
|
|
3048
|
+
var NextResourceApiBuilder = class extends NextAccelerateBuilder {
|
|
3049
|
+
constructor(inputName, options) {
|
|
3050
|
+
super(inputName, options);
|
|
3051
|
+
}
|
|
3052
|
+
setDefaultPath() {
|
|
3053
|
+
this.basePath = path8.join(process.cwd(), "src/app/api");
|
|
3054
|
+
if (pathExists(this.basePath)) return this;
|
|
3055
|
+
createDir(this.basePath);
|
|
3056
|
+
return this;
|
|
3057
|
+
}
|
|
3058
|
+
setBasePath() {
|
|
3059
|
+
this.basePath = path8.join(process.cwd(), "src/app/api", this.resource);
|
|
3060
|
+
if (pathExists(this.basePath)) return this;
|
|
3061
|
+
createDir(this.basePath);
|
|
3062
|
+
return this;
|
|
3063
|
+
}
|
|
3064
|
+
createCommonsApi() {
|
|
3065
|
+
const apiBase = path8.join(process.cwd(), "src/app/api");
|
|
3066
|
+
const imagesBase = path8.join(apiBase, "images");
|
|
3067
|
+
const deleteBase = path8.join(apiBase, "delete");
|
|
3068
|
+
if (pathExists(imagesBase) || pathExists(deleteBase)) {
|
|
3069
|
+
console.log("\x1B[36m\u2139 Common API routes already initialized. Skipping...\x1B[0m");
|
|
3070
|
+
return this;
|
|
3071
|
+
}
|
|
3072
|
+
const imageRoutePath = path8.join(imagesBase, "[resource]", "[resourceId]");
|
|
3073
|
+
createDir(imageRoutePath);
|
|
3074
|
+
createFile(path8.join(imageRoutePath, "route.ts"), imageUploadTemplate());
|
|
3075
|
+
const deleteRoutePath = path8.join(deleteBase, "[resource]", "[resourceId]");
|
|
3076
|
+
createDir(deleteRoutePath);
|
|
3077
|
+
createFile(path8.join(deleteRoutePath, "route.ts"), deleteTemplate());
|
|
3078
|
+
if (this.options?.git) this.createCommit("feat(api): add common resource routes");
|
|
3079
|
+
return this;
|
|
3080
|
+
}
|
|
3081
|
+
createDefaultUsersApi() {
|
|
3082
|
+
const apiBase = path8.join(process.cwd(), "src/app/api/users");
|
|
3083
|
+
createDir(apiBase);
|
|
3084
|
+
createFile(path8.join(apiBase, "route.ts"), usersCreateTemplate());
|
|
3085
|
+
const photoPath = path8.join(apiBase, "photo");
|
|
3086
|
+
createDir(photoPath);
|
|
3087
|
+
createFile(path8.join(photoPath, "route.ts"), usersPhotoTemplate());
|
|
3088
|
+
const updatePath = path8.join(apiBase, "update");
|
|
3089
|
+
createDir(updatePath);
|
|
3090
|
+
createFile(path8.join(updatePath, "route.ts"), usersUpdateTemplate());
|
|
3091
|
+
if (this.options?.git) this.createCommit("feat(api): add resources for users api");
|
|
3092
|
+
return this;
|
|
3093
|
+
}
|
|
3094
|
+
createResourceCrudApi() {
|
|
3095
|
+
const resourceBase = path8.join(process.cwd(), "src/app/api", this.resource);
|
|
3096
|
+
createDir(resourceBase);
|
|
3097
|
+
createFile(path8.join(resourceBase, "route.ts"), createResourceTemplate(capitalize(this.resource)));
|
|
3098
|
+
const paramFolder = `[${this.singular}Id]`;
|
|
3099
|
+
const updatePath = path8.join(resourceBase, paramFolder);
|
|
3100
|
+
createDir(updatePath);
|
|
3101
|
+
createFile(path8.join(updatePath, "route.ts"), updateResourceTemplate(capitalize(this.singular), capitalize(this.resource)));
|
|
3102
|
+
if (this.options?.git) this.createCommit(`feat(api/${this.resource}): add create and update routes`);
|
|
3103
|
+
return this;
|
|
3104
|
+
}
|
|
3105
|
+
};
|
|
3106
|
+
|
|
3107
|
+
// src/commands/create-api-resource.ts
|
|
3108
|
+
function createCommonsApiResource(options) {
|
|
3109
|
+
nextProjectGuardSimple();
|
|
3110
|
+
const builder = new NextResourceApiBuilder("default", options);
|
|
3111
|
+
builder.setDefaultPath().createCommonsApi().createDefaultUsersApi().build();
|
|
3112
|
+
console.log(`the resources "$commons created \x1B[32m\u2714 Success\x1B[0m`);
|
|
3113
|
+
}
|
|
3114
|
+
function createApiResource(inputName, options) {
|
|
3115
|
+
nextProjectGuardSimple();
|
|
3116
|
+
if (!inputName) {
|
|
3117
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mPlease provide the resource name.");
|
|
3118
|
+
process.exit(1);
|
|
3119
|
+
}
|
|
3120
|
+
const builder = new NextResourceApiBuilder(inputName, options);
|
|
3121
|
+
builder.setBasePath().createCommonsApi().createDefaultUsersApi().createResourceCrudApi().build();
|
|
3122
|
+
console.log(`the resources "${inputName}" created \x1B[32m\u2714 Success\x1B[0m`);
|
|
3123
|
+
}
|
|
3124
|
+
|
|
3125
|
+
// src/utils/interceptors/args.interceptor.ts
|
|
3126
|
+
var ALLOWED_FLAGS = /* @__PURE__ */ new Set(["--git", "--test", "--json"]);
|
|
3127
|
+
function useArgsInterceptor(args) {
|
|
3128
|
+
const [, , ...input] = args;
|
|
3129
|
+
if (input.length === 0) {
|
|
3130
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNo command was entered.");
|
|
3131
|
+
process.exit(1);
|
|
3132
|
+
}
|
|
3133
|
+
const command = input[0];
|
|
3134
|
+
if (command.startsWith("-") && command !== "-help") {
|
|
3135
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInvalid command");
|
|
3136
|
+
process.exit(1);
|
|
3137
|
+
}
|
|
3138
|
+
const rest = input.slice(1);
|
|
3139
|
+
let resource;
|
|
3140
|
+
const flags = [];
|
|
3141
|
+
for (const token of rest) {
|
|
3142
|
+
if (token.startsWith("-")) {
|
|
3143
|
+
if (ALLOWED_FLAGS.has(token)) {
|
|
3144
|
+
flags.push(token);
|
|
3145
|
+
continue;
|
|
3146
|
+
}
|
|
3147
|
+
if (!resource) {
|
|
3148
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mInvalid resource name");
|
|
3149
|
+
process.exit(1);
|
|
3150
|
+
}
|
|
3151
|
+
console.error(`\x1B[31m \u2716 Erro \x1B[0mUnknown flag: ${token}`);
|
|
3152
|
+
process.exit(1);
|
|
3153
|
+
}
|
|
3154
|
+
if (!resource) {
|
|
3155
|
+
resource = token;
|
|
3156
|
+
continue;
|
|
3157
|
+
}
|
|
3158
|
+
console.error("\x1B[31m \u2716 Erro \x1B[0mNo additional arguments are allowed.");
|
|
3159
|
+
process.exit(1);
|
|
3160
|
+
}
|
|
3161
|
+
return { command, resource, flags };
|
|
3162
|
+
}
|
|
3163
|
+
|
|
3164
|
+
// src/index.ts
|
|
3165
|
+
function main(args) {
|
|
3166
|
+
const { command, resource, flags } = useArgsInterceptor(args);
|
|
3167
|
+
const options = {
|
|
3168
|
+
git: flags.includes("--git"),
|
|
3169
|
+
test: flags.includes("--test"),
|
|
3170
|
+
json: flags.includes("--json")
|
|
3171
|
+
};
|
|
3172
|
+
switch (command) {
|
|
3173
|
+
case "create":
|
|
3174
|
+
createResource(resource, options);
|
|
3175
|
+
break;
|
|
3176
|
+
case "create:components":
|
|
3177
|
+
createComponents(options);
|
|
3178
|
+
break;
|
|
3179
|
+
case "create:form":
|
|
3180
|
+
createFormForResource(resource, options);
|
|
3181
|
+
break;
|
|
3182
|
+
case "config:next-auth":
|
|
3183
|
+
createNextAutResource(options);
|
|
3184
|
+
break;
|
|
3185
|
+
case "create:api-commons":
|
|
3186
|
+
createCommonsApiResource(options);
|
|
3187
|
+
break;
|
|
3188
|
+
case "create:api-resource":
|
|
3189
|
+
createApiResource(resource, options);
|
|
3190
|
+
break;
|
|
3191
|
+
case "config:tests-unit":
|
|
3192
|
+
createTestUnitConfig(options);
|
|
3193
|
+
break;
|
|
3194
|
+
case "config:tests-e2e":
|
|
3195
|
+
createTestE2EConfig(options);
|
|
3196
|
+
break;
|
|
3197
|
+
case "-help":
|
|
3198
|
+
console.log("\n\n\x1B[35mcommands available in the cli: \x1B[0m");
|
|
3199
|
+
console.log(" \x1B[32mcreate\x1B[0m \x1B[33mresource_name\x1B[0m -> creates all folders for a new resource.");
|
|
3200
|
+
console.log(" \x1B[32mcreate:components\x1B[0m -> create components resource.");
|
|
3201
|
+
console.log(" \x1B[32mcreate:form\x1B[0m \x1B[33mresource_name\x1B[0m -> creates a new form for the resource.");
|
|
3202
|
+
console.log(" \x1B[32mconfig:next-auth\x1B[0m -> create configuration for next auth.");
|
|
3203
|
+
console.log(" \x1B[32mcreate:api-commons\x1B[0m -> create common resources for using api routes.");
|
|
3204
|
+
console.log(" \x1B[32mcreate:api-resource\x1B[0m \x1B[33mresource-name\x1B[0m -> creates a new api resource.");
|
|
3205
|
+
console.log(" \x1B[32mconfig:tests-unit\x1B[0m -> config tests unit with vitest.");
|
|
3206
|
+
console.log(" \x1B[32mconfig:tests-e2e\x1B[0m -> config tests end to end with playwright.");
|
|
3207
|
+
console.log("\n\n");
|
|
3208
|
+
break;
|
|
3209
|
+
default:
|
|
3210
|
+
console.log("command unavailable in the cli...");
|
|
3211
|
+
}
|
|
3212
|
+
;
|
|
3213
|
+
}
|
|
3214
|
+
main(process.argv);
|
|
3215
|
+
export {
|
|
3216
|
+
main
|
|
3217
|
+
};
|